Skip to content

Commit

Permalink
Make group approvals conditional based on config (#674)
Browse files Browse the repository at this point in the history
* Update group approvals config format

* Update docs to account for group approvals

* Add property to Config; update API

* Make approver groups conditional

* Add dashboard announcement

* Make Group callout conditional

* Update groups API

* Remove commented-out code

---------

Co-authored-by: Jeff Daley <[email protected]>
  • Loading branch information
jfreda and jeffdaley authored Apr 12, 2024
1 parent 51a2a10 commit 52bbe69
Show file tree
Hide file tree
Showing 13 changed files with 118 additions and 40 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Hermes was created and is currently maintained by HashiCorp Labs, a small team i

1. Enable the following APIs for [Google Workspace APIs](https://developers.google.com/workspace/guides/enable-apis)

- Admin SDK API
- Admin SDK API (optional, if enabling Google Groups as document approvers)
- Google Docs API
- Google Drive API
- Gmail API
Expand Down Expand Up @@ -146,12 +146,12 @@ NOTE: when not using a Google service account, this will automatically open a br

- Create a new key (JSON type) for the service account and download it.
- Go to [Delegating domain-wide authority to the service account](https://developers.google.com/identity/protocols/oauth2/service-account#delegatingauthority) and follow the instructions to enter the OAuth scopes.
- Add the following OAuth scopes (comma-delimited list):
- Add the following OAuth scopes (if enabling group approvals, add `https://www.googleapis.com/auth/admin.directory.group.readonly` to the comma-delimited list):
`https://www.googleapis.com/auth/directory.readonly,https://www.googleapis.com/auth/documents,https://www.googleapis.com/auth/drive,https://www.googleapis.com/auth/gmail.send`

1. Configure the service account in the `auth` block under the `google_workspace` config block.

More to come here...
1. If enabling group approvals, add the `https://www.googleapis.com/auth/admin.directory.group.readonly` role to the service user configured as the `subject` in the `auth` block (from previous step).

## Architecture

Expand Down
11 changes: 9 additions & 2 deletions configs/config.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,15 @@ google_workspace {
// drafts_folder contains all draft documents.
drafts_folder = "my-drafts-folder-id"

// groups_prefix is the prefix to use when searching for Google Groups.
// groups_prefix = "team-"
// group_approvals is the configuration for using Google Groups as document
// approvers.
group_approvals {
// enabled enables using Google Groups as document approvers.
enabled = false

// search_prefix is the prefix to use when searching for Google Groups.
// search_prefix = "team-"
}

// If create_doc_shortcuts is set to true, shortcuts_folder will contain an
// organized hierarchy of folders and shortcuts to published files that can be
Expand Down
17 changes: 15 additions & 2 deletions internal/api/v2/groups.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@ func GroupsHandler(srv server.Server) http.Handler {
return
}

// Respond with error if group approvals are not enabled.
if srv.Config.GoogleWorkspace.GroupApprovals == nil ||
!srv.Config.GoogleWorkspace.GroupApprovals.Enabled {
http.Error(w,
"Group approvals have not been enabled", http.StatusUnprocessableEntity)
return
}

switch r.Method {
case "POST":
// Decode request.
Expand All @@ -73,11 +81,16 @@ func GroupsHandler(srv server.Server) http.Handler {
)

// Retrieve groups with prefix, if configured.
if srv.Config.GoogleWorkspace.GroupsPrefix != "" {
searchPrefix := ""
if srv.Config.GoogleWorkspace.GroupApprovals != nil &&
srv.Config.GoogleWorkspace.GroupApprovals.SearchPrefix != "" {
searchPrefix = srv.Config.GoogleWorkspace.GroupApprovals.SearchPrefix
}
if searchPrefix != "" {
maxNonPrefixGroups = maxGroupResults - maxPrefixGroupResults

prefixQuery := fmt.Sprintf(
"%s%s", srv.Config.GoogleWorkspace.GroupsPrefix, query)
"%s%s", searchPrefix, query)
prefixGroups, err = srv.GWService.AdminDirectory.Groups.List().
Domain(srv.Config.GoogleWorkspace.Domain).
MaxResults(maxPrefixGroupResults).
Expand Down
15 changes: 13 additions & 2 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,8 +219,9 @@ type GoogleWorkspace struct {
// DraftsFolder is the folder that contains all document drafts.
DraftsFolder string `hcl:"drafts_folder"`

// GroupsPrefix is the prefix to use when searching for Google Groups.
GroupsPrefix string `hcl:"groups_prefix,optional"`
// GoogleWorkspaceGroupApprovals is the configuration for using Google Groups as
// document approvers.
GroupApprovals *GoogleWorkspaceGroupApprovals `hcl:"group_approvals,block"`

// OAuth2 is the configuration to use OAuth 2.0 to access Google Workspace
// APIs.
Expand All @@ -241,6 +242,16 @@ type GoogleWorkspace struct {
UserNotFoundEmail *GoogleWorkspaceUserNotFoundEmail `hcl:"user_not_found_email,block"`
}

// GoogleWorkspaceGroupApprovals is the configuration for using Google Groups as
// document approvers.
type GoogleWorkspaceGroupApprovals struct {
// Enabled enables using Google Groups as document approvers.
Enabled bool `hcl:"enabled,optional"`

// SearchPrefix is the prefix to use when searching for Google Groups.
SearchPrefix string `hcl:"search_prefix,optional"`
}

// GoogleWorkspaceOAuth2 is the configuration to use OAuth 2.0 to access Google
// Workspace APIs.
type GoogleWorkspaceOAuth2 struct {
Expand Down
45 changes: 28 additions & 17 deletions web/app/components/dashboard/new-features-banner.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,39 @@
data-test-new-features-banner
@type="inline"
@color="highlight"
@icon="folder-star"
{{! Icon is hidden by CSS; See `dashboard.scss` }}
@onDismiss={{this.dismiss}}
class="mb-10"
as |A|
>
<A.Title>Introducing Projects!</A.Title>
<A.Title>What's new in Hermes</A.Title>
<A.Description>
Projects are a new way to organize documents and links around an effort.
<div class="mt-2 mb-1 flex items-center gap-2">
<Hds::Button
@route="authenticated.projects"
@text="Browse projects"
@size="small"
/>
<span class="hermes-h4">or</span>
<Hds::Button
@route="authenticated.new.project"
@text="Start a project"
@color="secondary"
@size="small"
/>
</div>
<ul class="icon-list">
{{#if this.configSvc.config.group_approvals}}
<li>
<FlightIcon @name="users" />
<p>
Google Groups can be added as document approvers
</p>
</li>
{{/if}}
<li>
<FlightIcon @name="swap-horizontal" />
<p>
Document ownership can be transferred between users
</p>
</li>
<li>
<FlightIcon @name="smile" />
<p>
We've improved owner filtering on the
<LinkTo @route="authenticated.documents" class="underlined-link">
All Docs
</LinkTo>
view
</p>
</li>
</ul>
</A.Description>
</Hds::Alert>
{{/if}}
9 changes: 8 additions & 1 deletion web/app/components/dashboard/new-features-banner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,22 @@ import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import window from "ember-window-mock";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import ConfigService from "hermes/services/config";

export const NEW_FEATURES_BANNER_LOCAL_STORAGE_ITEM =
"jan-18-2024-newFeatureBannerIsShown";
"apr-12-2024-newFeatureBannerIsShown";

interface DashboardNewFeaturesBannerSignature {
Args: {};
}

export default class DashboardNewFeaturesBanner extends Component<DashboardNewFeaturesBannerSignature> {
/**
* Used to determine whether the Google Groups callout should be shown.
*/
@service("config") declare configSvc: ConfigService;

@tracked protected isDismissed = false;

/**
Expand Down
4 changes: 2 additions & 2 deletions web/app/components/document/sidebar.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@
@onSave={{perform this.saveApprovers}}
@isSaving={{this.saveIsRunning}}
@isReadOnly={{this.editingIsDisabled}}
@includeGroupsInPeopleSelect={{true}}
@includeGroupsInPeopleSelect={{this.configSvc.config.group_approvals}}
{{! Provide the document to the `has-approved-doc` helper }}
@document={{@document}}
/>
Expand Down Expand Up @@ -767,7 +767,7 @@
<Hds::Form::Field @layout="vertical" as |F|>
<F.Control>
<Inputs::PeopleSelect
@includeGroups={{true}}
@includeGroups={{this.configSvc.config.group_approvals}}
@renderInPlace={{true}}
@selected={{this.allApprovers}}
@onChange={{this.updateApprovers}}
Expand Down
1 change: 1 addition & 0 deletions web/app/config/environment.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export interface HermesConfig {
};
shortLinkBaseURL: string;
skipGoogleAuth: boolean;
groupApprovals: boolean;
showEmberAnimatedTools: boolean;
supportLinkURL: string;
version: string;
Expand Down
24 changes: 13 additions & 11 deletions web/app/routes/authenticated/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,17 +157,19 @@ export default class AuthenticatedDocumentRoute extends Route {

// Check if the user is a group approver.

const resp = await this.fetchSvc
.fetch(
`/api/${this.configSvc.config.api_version}/approvals/${params.document_id}`,
{ method: "OPTIONS" },
)
.then((r) => r);

const allowed = resp?.headers.get("allowed");

if (allowed?.includes("POST")) {
viewerIsGroupApprover = true;
if (this.configSvc.config.group_approvals) {
const resp = await this.fetchSvc
.fetch(
`/api/${this.configSvc.config.api_version}/approvals/${params.document_id}`,
{ method: "OPTIONS" },
)
.then((r) => r);

const allowed = resp?.headers.get("allowed");

if (allowed?.includes("POST")) {
viewerIsGroupApprover = true;
}
}

const typedDoc = doc as HermesDocument;
Expand Down
1 change: 1 addition & 0 deletions web/app/services/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export default class ConfigService extends Service {
support_link_url: config.supportLinkURL,
version: config.version,
short_revision: config.shortRevision,
group_approvals: config.groupApprovals,
};

setConfig(param: HermesConfig) {
Expand Down
15 changes: 15 additions & 0 deletions web/app/styles/components/dashboard.scss
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,18 @@
@apply mt-0;
}
}

.hds-alert--color-highlight {
@apply pl-5;

.hds-alert__icon {
@apply hidden;
}
}

.icon-list {
li {
@apply grid items-center gap-2.5 py-0.5 pl-1;
grid-template-columns: 16px 1fr;
}
}
1 change: 1 addition & 0 deletions web/mirage/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export const TEST_WEB_CONFIG = {
google_doc_folders: "",
short_link_base_url: TEST_SHORT_LINK_BASE_URL,
skip_google_auth: false,
group_approvals: true,
google_analytics_tag_id: undefined,
support_link_url: TEST_SUPPORT_URL,
version: "1.2.3",
Expand Down
9 changes: 9 additions & 0 deletions web/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ type ConfigResponse struct {
GoogleAnalyticsTagID string `json:"google_analytics_tag_id"`
GoogleOAuth2ClientID string `json:"google_oauth2_client_id"`
GoogleOAuth2HD string `json:"google_oauth2_hd"`
GroupApprovals bool `json:"group_approvals"`
JiraURL string `json:"jira_url"`
ShortLinkBaseURL string `json:"short_link_base_url"`
SkipGoogleAuth bool `json:"skip_google_auth"`
Expand Down Expand Up @@ -120,6 +121,13 @@ func ConfigHandler(
createDocsAsUser = true
}

// Set GroupApprovals if enabled in the config.
groupApprovals := false
if cfg.GoogleWorkspace.GroupApprovals != nil &&
cfg.GoogleWorkspace.GroupApprovals.Enabled {
groupApprovals = true
}

// Set JiraURL if enabled in the config.
jiraURL := ""
if cfg.Jira != nil && cfg.Jira.Enabled {
Expand All @@ -136,6 +144,7 @@ func ConfigHandler(
GoogleAnalyticsTagID: cfg.GoogleAnalyticsTagID,
GoogleOAuth2ClientID: cfg.GoogleWorkspace.OAuth2.ClientID,
GoogleOAuth2HD: cfg.GoogleWorkspace.OAuth2.HD,
GroupApprovals: groupApprovals,
JiraURL: jiraURL,
ShortLinkBaseURL: shortLinkBaseURL,
SkipGoogleAuth: skipGoogleAuth,
Expand Down

0 comments on commit 52bbe69

Please sign in to comment.