diff --git a/group-membership/.eslintrc.json b/group-membership/.eslintrc.json new file mode 100644 index 00000000..9f81d854 --- /dev/null +++ b/group-membership/.eslintrc.json @@ -0,0 +1,6 @@ +{ + "parserOptions": { + "ecmaVersion": 6 + }, + "extends": "../configs/appsscript.eslintrc.json" +} diff --git a/group-membership/README.md b/group-membership/README.md new file mode 100644 index 00000000..3de75865 --- /dev/null +++ b/group-membership/README.md @@ -0,0 +1,119 @@ +--- +title: Scale access for external vendors and partners +description: Share documents, events, and communications with users outside +your domain +labels: Sheets, Groups, Apps Script +material_icon: group_work +create_time: 2019-08-30 +update_time: 2020-3-27 +--- +Contributed by Tech and Eco, follow me on +[Twitter](https://twitter.com/TechandEco)! + +Use a Google Group to work cross-functionally with vendors, partners, +customers, and volunteers outside of your domain, and scale access +to work assignments and leverage security controls. You can add an external +user's Google email (this can be their own G Suite account, a G Suite user +account you created for them within your domain, or a free Gmail account) to +your G Suite Google group (not available for consumer Google Groups) via a +Google Sheet to manage users in bulk and keep track of the onboarding history. + +Top 6 benefits of this solution: + + 1. Adds a new external user to a G Suite Google Group you create + (example: externalteam@mydomain.com) once, and it allows users to open all + resources that are shared with that group moving forward (calendar, files, + training site, dashboards (built on DataStudio), etc. + + 1. When adding one or more users to your Google Group, users can receive a + welcome email with the links they can access. + [Creating a Google Site](https://sites.google.com/new) is optional but + recommended to centralize information in a beautiful website interface. + + 1. The + [template of the welcome email](https://docs.google.com/document/d/1GZDh_u9B2ARpYs3Iks8ZouQ_Xlixq076kAst710Z4cg/edit?usp=sharing) + is composed in a Google Doc, and the script renders it as HTML. You can use + seperate Google Docs for different recipients to customize messaging. + + 1. Capture the email addresses of users in your Sheet, and add them in bulk to + the desired Google Groups at a later time by changing the column "Allowed" + to "yes" for each row of users. + + 1. You or members of your team, can add users to different Google Groups as + long as the person who made a copy of this sheet has + _manager_ or _owner_ rights in each of those Google Groups. + + 1. Removing a user from a Group (via the Google Group's interface) will revoke + their access to all resources. You can also limit documents to be available + to that group for a + [specific period of time](https://support.google.com/docs/answer/2494893?co=GENIE.Platform%3DDesktop&hl=en). + +![Users are added to a Google Group via a Google Sheet](https://cdn.jsdelivr.net/gh/gsuitedevs/solutions@master/group-membership/demo.gif) + +## Technology highlights + +- Install a trigger with a click so the script is setup to run everytime the +Sheet is edited. To learn more visit the + [Apps Script trigger guide](https://developers.google.com/apps-script/guides/triggers/installable)) +- Add members to a _Google Group for Business_ using the + [Admin Directory API](https://developers.google.com/apps-script/advanced/admin-sdk-directory) + +## Try it + +1. Copy the [External Sustainability Group List](https://docs.google.com/spreadsheets/d/1toqdDkWSAOL7aIElil59RHJH0b1Efebg7GBlgjn3B2Y/copy) from your _G Suite_ account. + +1. Enter for testing purposes a _Gmail address you own_ and a Google Group you + have _rights to manage_ its membership. You can learn about + [group permissions here](https://support.google.com/groups/answer/2464975?hl=en) + + > _Note_: The membership status will be populated by the words + > **Newly added** if the user was added to the group, or **Already added** + > if it recognizes the user is already a member of that group. + +1. Enter “Yes” in the “Allowed” column. + +1. Next, click the custom menu called **Install Trigger** > **onEdit**, this + will install a trigger that will run everytime a value is changed in the + sheet. + + ![Install trigger screenshot](https://cdn.jsdelivr.net/gh/gsuitedevs/solutions@master/group-membership/install_trigger.png) + +1. When prompted, click the **Review permissions** and click **Allow** so the + script can email on your behalf. + + > _Note_: If you get a warning that **This app isn't verified** continue + > with the verification process by clicking **Advanced** and then scroll + > down and click the grey text at the bottom that begins with **Go to...** + +1. Your status field will change. + +![Status column is changed](https://cdn.jsdelivr.net/gh/gsuitedevs/solutions@master/group-membership/final_group_add.gif) + +1. Check your inbox to see the email, and then your Google Group’s interface + to see the new member added. + +## _[optional]_ Customize your messaging + +- If you wish to change the subject lines of your emails, replace + the text in the “Email subject” column. + +- If you wish to change the + [email template](https://docs.google.com/document/d/1GZDh_u9B2ARpYs3Iks8ZouQ_Xlixq076kAst710Z4cg/edit#heading=h.uwtpzkmp9874) + that is sent out, replace the URL in the “Email template” column with your + preferred Google Doc. If you wish to include any of the column values in the + template, enter them as such in the template {{Column_name}} like + this: _Welcome, we have added your {{Email}} to this {{Google_Group}} in + order give you access to the following resources..._ + > _Note_: If you encounter any issues with the welcome email, change the + > permission levels of the Google Doc templates to more open settings. + +## Next steps + +To get started with Google Apps Script, try out [the codelab][codelab] +which guides you through the creation of your first script. + +You can also view the [full source code][github] of this solution on GitHub to +learn more about how it was built. + +[codelab]: https://codelabs.developers.google.com/codelabs/apps-script-intro +[github]: https://github.com/gsuitedevs/solutions/blob/master/group-membership diff --git a/group-membership/data/sheet-data.xlsx b/group-membership/data/sheet-data.xlsx new file mode 100644 index 00000000..678bebcb Binary files /dev/null and b/group-membership/data/sheet-data.xlsx differ diff --git a/group-membership/demo.gif b/group-membership/demo.gif new file mode 100644 index 00000000..01b8422d Binary files /dev/null and b/group-membership/demo.gif differ diff --git a/group-membership/final_group_add.gif b/group-membership/final_group_add.gif new file mode 100644 index 00000000..d6aaba0b Binary files /dev/null and b/group-membership/final_group_add.gif differ diff --git a/group-membership/install_trigger.png b/group-membership/install_trigger.png new file mode 100644 index 00000000..ac5a83bb Binary files /dev/null and b/group-membership/install_trigger.png differ diff --git a/group-membership/src/Code.js b/group-membership/src/Code.js new file mode 100644 index 00000000..636940d9 --- /dev/null +++ b/group-membership/src/Code.js @@ -0,0 +1,264 @@ +const EMAIL = 'Email'; +const GOOGLE_GROUP = 'Google Group'; +const ALLOWED = 'Allowed'; +const EMAIL_TEMPLATE_DOC_URL = 'Email template doc URL'; +const EMAIL_SUBJECT = 'Email subject'; +const EMAIL_STATE = 'Email state'; + +/** + * Email state base class. + */ +class EmailState { + /** + * Converts state to string. + * @return {string} + */ + toString() { + return ''; + } +} + +/** + * 'Email sent' state. + */ +class StateSent extends EmailState { + /** + * Class constructor. + * @param {Date} date - date when the email was sent. + */ + constructor(date) { + super(); + this.date = date; + } + /** + * Converts state to string. + * @return {string} + */ + toString() { + return `Sent: ${this.date}`; + } +} + +/** + * 'Already in group' state. + */ +class StateAlreadyInGroup extends EmailState { + /** + * Converts state to string. + * @return {string} + */ + toString() { + return 'Already in group'; + } +} + +/** + * 'Not sent' state. + */ +class StateNotSent extends EmailState { + /** + * Converts state to string. + * @return {string} + */ + toString() { + return 'Not sent'; + } +} + +/** + * 'Requiered field missing' state. + */ +class StateRequiredFieldMissing extends EmailState { + /** + * Class constructor. + * @param {string} user - email of user to add to the group. + * @param {string} group - address of Google Group to add user to. + * @param {string} template - Google Doc URL that serves as template + * of the welcome email sent to a user added to the group. + * @param {string} subject - subject of welcome email sent to user added + */ + constructor(user, group, template, subject) { + super(); + this.user = user; + this.group = group; + this.template = template; + this.subject = subject; + } + /** + * Converts state to string. + * @return {string} + */ + toString() { + return `Required field missing: ${ + !this.user ? EMAIL : + !this.group ? GOOGLE_GROUP : + !this.template ? EMAIL_TEMPLATE_DOC_URL : + EMAIL_SUBJECT}`; + } +} + +/** + * 'Allowed field not specified' state. + */ +class StateAllowedFieldNotSpecified extends EmailState {} + +/** + * 'Error' state. + */ +class StateError extends EmailState { + /** + * Class constructor. + * @param {Exception} error - error message. + */ + constructor(error) { + super(); + this.error = error; + } + /** + * Converts state to string. + * @return {string} + */ + toString() { + return `${this.error}`; + } +} + +/** + * Installs a trigger in the Spreadsheet to run upon the Sheet being opened. + * To learn more about triggers read: + * https://developers.google.com/apps-script/guides/triggers + */ +function onOpen() { + SpreadsheetApp.getUi() + .createMenu('Install trigger') + .addItem('onEdit', 'installOnEditTrigger') + .addToUi(); +} + +/** + * Installs a trigger in the Spreadsheet that is scheduled + * to run upon when values in the Sheet are edited. + * To learn more about triggers read: + * https://developers.google.com/apps-script/guides/triggers/installable + */ +function installOnEditTrigger() { + ScriptApp.newTrigger('onEditInstallableTrigger') + .forSpreadsheet(SpreadsheetApp.getActive()) + .onEdit() + .create(); +} + +/** + * Trigger that runs on edit after being installed via the interface. + * + * @param {Object} e - onEdit trigger event. + */ +function onEditInstallableTrigger(e) { + // Don't do anything if the header row (first row) was updated. + if (e.range.getRow() == 1) { + return; + } + + // Get the headers, row range and values from the active sheet. + const sheet = SpreadsheetApp.getActiveSheet(); + const headers = sheet.getDataRange().offset(0, 0, 1).getValues()[0]; + const range = sheet.getRange(e.range.getRow(), 1, 1, headers.length); + const row = range.getValues()[0]; + + // Convert the row Array into an entries Object using the headers for the + // field names. + const entries = headers.reduce((result, columnName, i) => { + result[columnName] = row[i]; + return result; + }, {}); + + // Update the entries Object with the email state returned by addToGroup(). + try { + entries[EMAIL_STATE] = addToGroup( + entries[EMAIL], + entries[GOOGLE_GROUP], + entries[ALLOWED], + entries[EMAIL_TEMPLATE_DOC_URL], + entries[EMAIL_SUBJECT] + ).toString(); + } catch (e) { + // If there's an error, report that as the email state. + entries[EMAIL_STATE] = new StateError(e).toString(); + } + + // Convert the updated entries Object into a row Array. + const rowToWrite = headers.map((columnName) => entries[columnName]); + + // setValues() receives a 2D array, so we create an array with the row + // contents. + console.log(JSON.stringify(rowToWrite)); + range.setValues([rowToWrite]); +} + + +/** + * Trigger that runs on edit after being installed via the interface. + * + * @param {string} userEmail - email of user to add to the group. + * @param {string} groupEmail - address of Google Group to add user to. + * @param {string} allowed - 'yes' flag to add user to group. + * @param {string} emailTemplateDocUrl - Google Doc URL that serves as template + * of the welcome email sent to a user added to the group. + * @param {string} emailSubject - subject of welcome email sent to user added + * to group. + * @return {EmailState} - state if email was sent to a user added in the sheet. + */ +function addToGroup(userEmail, groupEmail, allowed, emailTemplateDocUrl, emailSubject) { + if (!allowed) { + return new StateAllowedFieldNotSpecified(); + } + if (!userEmail || !groupEmail || !emailTemplateDocUrl || !emailSubject) { + return new StateRequiredFieldMissing( + userEmail, groupEmail, emailTemplateDocUrl, emailSubject); + } + if (allowed.toLowerCase() != 'yes') { + return new StateNotSent(); + } + + // If the group does not contain the user's email, add it and send an email. + const group = GroupsApp.getGroupByEmail(groupEmail); + if (!group.hasUser(userEmail)) { + // User is not part of the group, add user to the group. + const member = {email: userEmail, role: 'MEMBER'}; + AdminDirectory.Members.insert(member, groupEmail); + + // Send a confirmation email that the member was now added. + const docId = DocumentApp.openByUrl(emailTemplateDocUrl).getId(); + + // Replace the template variables like {{VARIABLE}} with real values. + const emailBody = docToHtml(docId) + .replace('{{EMAIL}}', userEmail) + .replace('{{GOOGLE_GROUP}}', groupEmail); + + MailApp.sendEmail({ + to: userEmail, + subject: emailSubject, + htmlBody: emailBody, + }); + + return new StateSent(new Date()); + } + return new StateAlreadyInGroup(); +} + +/** + * Fetches a Google Doc as an HTML string. + * + * @param {string} docId - The ID of a Google Doc to fetch content from. + * @return {string} The Google Doc rendered as an HTML string. + */ +function docToHtml(docId) { + const url = 'https://docs.google.com/feeds/download/documents/export/Export?id=' + + docId + '&exportFormat=html'; + const param = { + method: 'get', + headers: {'Authorization': 'Bearer ' + ScriptApp.getOAuthToken()}, + muteHttpExceptions: true, + }; + return UrlFetchApp.fetch(url, param).getContentText(); +} diff --git a/group-membership/src/appsscript.json b/group-membership/src/appsscript.json new file mode 100644 index 00000000..f526cf6c --- /dev/null +++ b/group-membership/src/appsscript.json @@ -0,0 +1,22 @@ +{ + "timeZone": "America/Los_Angeles", + "dependencies": { + "enabledAdvancedServices": [{ + "userSymbol": "AdminDirectory", + "serviceId": "admin", + "version": "directory_v1" + }] + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8", + "oauthScopes": [ + "https://www.googleapis.com/auth/admin.directory.group.member", + "https://www.googleapis.com/auth/documents", + "https://www.googleapis.com/auth/drive.readonly", + "https://www.googleapis.com/auth/groups", + "https://www.googleapis.com/auth/script.external_request", + "https://www.googleapis.com/auth/script.scriptapp", + "https://www.googleapis.com/auth/script.send_mail", + "https://www.googleapis.com/auth/spreadsheets.currentonly" + ] +}