Skip to content
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

Make group approvals conditional based on config #674

Merged
merged 8 commits into from
Apr 12, 2024
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
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
Loading