Skip to content

Commit

Permalink
[CLD-6678] Various improvements for IP filtering feature (mattermost#…
Browse files Browse the repository at this point in the history
…25485)

* Add GetInstallation function, allow IP Filtering page to fetch installation state, other fixes for IP filter feature

* Fix pipelines

* Run make build-templates

* Fixing i18n

* Fix openapi docs

* Fix openapi docs again

* make build-templates

* Update test to ensure that spinner is removed after installation becomes stable

* Fix types, style

* update openapi because I can't validate locally...

* Updates according to Matt's feedback

* Add a limit to number of times installation is requested before an error is displayed

* Make button disable immediately

* Updates based on PR feedback

* A couple missed occurrences of whitespace

* Grammar fix in failed to fetch error

---------

Co-authored-by: Gabe Jackson <[email protected]>
Co-authored-by: Mattermost Build <[email protected]>
  • Loading branch information
3 people authored Nov 28, 2023
1 parent 894bba8 commit 95670ab
Show file tree
Hide file tree
Showing 16 changed files with 318 additions and 21 deletions.
30 changes: 30 additions & 0 deletions api/v4/source/cloud.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,36 @@
$ref: "#/components/responses/Forbidden"
"501":
$ref: "#/components/responses/NotImplemented"
/api/v4/cloud/installation:
get:
tags:
- cloud
summary: GET endpoint for Installation information
description: >
An endpoint for fetching the installation information.
##### Permissions
Must have `sysconsole_read_site_ip_filters` permission and be licensed for Cloud.
__Minimum server version__: 9.1
__Note:__ This is intended for internal use and is subject to change.
operationId: GetEndpointForInstallationInformation
responses:
"200":
description: Installation returned successfully
content:
application/json:
schema:
$ref: "#/components/schemas/Installation"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"501":
$ref: "#/components/responses/NotImplemented"
/api/v4/cloud/subscription/invoices:
get:
tags:
Expand Down
11 changes: 11 additions & 0 deletions api/v4/source/definitions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3524,6 +3524,17 @@ components:
Description:
description: A description for the CIDRBlock
type: string
Installation:
type: object
properties:
id:
description: A unique identifier
type: string
allowed_ip_ranges:
$ref: "#/components/schemas/AllowedIPRange"
state:
description: The current state of the installation
type: string
externalDocs:
description: Find out more about Mattermost
url: 'https://about.mattermost.com'
Expand Down
26 changes: 26 additions & 0 deletions server/channels/api4/cloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ func (api *API) InitCloud() {
// POST /api/v4/cloud/webhook
api.BaseRoutes.Cloud.Handle("/webhook", api.CloudAPIKeyRequired(handleCWSWebhook)).Methods("POST")

// GET /api/v4/cloud/installation
api.BaseRoutes.Cloud.Handle("/installation", api.APISessionRequired(getInstallation)).Methods("GET")

// GET /api/v4/cloud/cws-health-check
api.BaseRoutes.Cloud.Handle("/check-cws-connection", api.APIHandler(handleCheckCWSConnection)).Methods("GET")

Expand Down Expand Up @@ -474,6 +477,29 @@ func getCloudCustomer(c *Context, w http.ResponseWriter, r *http.Request) {
w.Write(json)
}

func getInstallation(c *Context, w http.ResponseWriter, r *http.Request) {
ensured := ensureCloudInterface(c, "Api4.getInstallation")
if !ensured {
return
}

if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadIPFilters) {
c.SetPermissionError(model.PermissionSysconsoleReadIPFilters)
return
}

installation, err := c.App.Cloud().GetInstallation(c.AppContext.Session().UserId)
if err != nil {
c.Err = model.NewAppError("Api4.getInstallation", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}

if err := json.NewEncoder(w).Encode(installation); err != nil {
c.Err = model.NewAppError("Api4.getInstallation", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
}

// getLicenseSelfServeStatus makes check for the license in the CWS self-serve portal and establishes if the license is renewable, expandable etc.
func getLicenseSelfServeStatus(c *Context, w http.ResponseWriter, r *http.Request) {
ensured := ensureCloudInterface(c, "Api4.getLicenseSelfServeStatus")
Expand Down
1 change: 1 addition & 0 deletions server/einterfaces/cloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,5 @@ type CloudInterface interface {

ApplyIPFilters(userID string, ranges *model.AllowedIPRanges) (*model.AllowedIPRanges, error)
GetIPFilters(userID string) (*model.AllowedIPRanges, error)
GetInstallation(userID string) (*model.Installation, error)
}
26 changes: 26 additions & 0 deletions server/einterfaces/mocks/CloudInterface.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions server/public/model/cloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,12 @@ type CreateSubscriptionRequest struct {
DiscountID string `json:"discount_id"`
}

type Installation struct {
ID string `json:"id"`
State string `json:"state"`
AllowedIPRanges *AllowedIPRanges `json:"allowed_ip_ranges"`
}

type Feedback struct {
Reason string `json:"reason"`
Comments string `json:"comments"`
Expand Down
4 changes: 2 additions & 2 deletions server/templates/ip_filters_changed.html
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@
<tbody>
<tr>
<td style="width:132px;">
<img alt height="21" src="{{.Props.SiteURL}}/static/images/logo_email_dark.png" style="border:0;display:block;outline:none;text-decoration:none;height:21.76px;width:100%;font-size:13px;" width="132">
<img alt height="21" src="{{.Props.PortalURL}}/static/images/logo_email_dark.png" style="border:0;display:block;outline:none;text-decoration:none;height:21.76px;width:100%;font-size:13px;" width="132">
</td>
</tr>
</tbody>
Expand Down Expand Up @@ -441,7 +441,7 @@
<tbody>
<tr>
<td style="width:312px;">
<img alt height="auto" src="{{.Props.SiteURL}}/static/images/forgot_password_illustration.png" style="border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;" width="312">
<img alt height="auto" src="{{.Props.PortalURL}}/static/images/forgot_password_illustration.png" style="border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;" width="312">
</td>
</tr>
</tbody>
Expand Down
19 changes: 11 additions & 8 deletions server/templates/ip_filters_changed.mjml
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,35 @@
</mj-head>
<mj-body css-class="emailBody" background-color="#FFFFFF">
<mj-wrapper mj-class="email">
<mj-include path="./partials/logo.mjml" />
<mj-section padding="0px 0px 40px 0px">
<mj-column>
<mj-image mj-class="logo" src="{{.Props.PortalURL}}/static/images/logo_email_dark.png" />
</mj-column>
</mj-section>
<mj-include path="./partials/header.mjml" />
<mj-section padding="0px">
<mj-column>
<mj-image src="{{.Props.SiteURL}}/static/images/forgot_password_illustration.png" width="312px"
padding="0px" />
<mj-image src="{{.Props.PortalURL}}/static/images/forgot_password_illustration.png" width="312px" padding="0px" />
</mj-column>
</mj-section>
<mj-section padding="40px 0px 40px 0px">
<mj-column>
<mj-text padding-bottom="9px" css-class="footerTitle" padding="0px">
{{.Props.TroubleAccessingTitle}}
{{.Props.TroubleAccessingTitle}}
</mj-text>
<mj-raw>{{if .Props.ActorEmail}}</mj-raw>
<mj-button padding-top="0px" padding-bottom="1px" font-size="14px" line-height="20px" background-color="transparent" color="#1C58D9" href="mailto:{{.Props.ActorEmail}}">
{{.Props.SendAnEmailTo}}
</mj-button>
<mj-divider padding="0" css-class="divider" width="313px" border-width="1px" border-color="#3F4350"/>
<mj-divider padding="0" css-class="divider" width="313px" border-width="1px" border-color="#3F4350" />
<mj-raw>{{end}}</mj-raw>
<mj-raw>{{ if .Props.LogInToCustomerPortal}}</mj-raw>
<mj-button padding-top="6px" padding-bottom="1px" font-size="14px" line-height="20px" background-color="transparent" color="#1C58D9" href="{{.Props.PortalURL}}/console/cloud/ip-filtering">
<mj-button padding-top="6px" padding-bottom="1px" font-size="14px" line-height="20px" background-color="transparent" color="#1C58D9" href="{{.Props.PortalURL}}/console/cloud/ip-filtering">
{{.Props.LogInToCustomerPortal}}
</mj-button>
<mj-divider padding="0px" css-class="divider" width="313px" border-width="1px" border-color="#3F4350"/>
<mj-divider padding="0px" css-class="divider" width="313px" border-width="1px" border-color="#3F4350" />
<mj-raw>{{end}}</mj-raw>
<mj-button padding-top="6px" font-size="14px" line-height="20px" background-color="transparent" color="#1C58D9" href="mailto:{{.Props.SupportEmail}}">
<mj-button padding-top="6px" font-size="14px" line-height="20px" background-color="transparent" color="#1C58D9" href="mailto:{{.Props.SupportEmail}}">
{{.Props.ContactSupport}}
</mj-button>
</mj-column>
Expand Down
11 changes: 11 additions & 0 deletions webapp/channels/src/actions/cloud.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,17 @@ export function completeStripeAddPaymentMethod(
};
}

export function getInstallation() {
return async () => {
try {
const installation = await Client4.getInstallation();
return {data: installation};
} catch (e: any) {
return {error: e.message};
}
};
}

export function subscribeCloudSubscription(
productId: string,
shippingAddress: Address = getBlankAddressWithCountry(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import {useDispatch} from 'react-redux';
import {AlertOutlineIcon} from '@mattermost/compass-icons/components';
import type {AllowedIPRange, FetchIPResponse} from '@mattermost/types/config';

import type {DispatchFunc} from 'mattermost-redux/types/actions';

import {applyIPFilters, getCurrentIP, getIPFilters} from 'actions/admin_actions';
import {getInstallation} from 'actions/cloud';
import {closeModal, openModal} from 'actions/views/modals';

import AdminHeader from 'components/widgets/admin_console/admin_header';
Expand All @@ -27,16 +30,35 @@ import SaveChangesPanel from '../team_channel_settings/save_changes_panel';
import './ip_filtering.scss';

const IPFiltering = () => {
const dispatch = useDispatch();
const dispatch = useDispatch<DispatchFunc>();
const {formatMessage} = useIntl();
const [ipFilters, setIpFilters] = useState<AllowedIPRange[] | null>(null);
const [originalIpFilters, setOriginalIpFilters] = useState<AllowedIPRange[] | null>(null);
const [saveNeeded, setSaveNeeded] = useState(false);
const [currentUsersIP, setCurrentUsersIP] = useState<string | null>(null);
const [saving, setSaving] = useState<boolean>(false);
const [filterToggle, setFilterToggle] = useState<boolean>(false);
const [installationStatus, setInstallationStatus] = useState<string>('');

// savingMessage allows the component to change the label on the Save button in the SaveChangesPanel
const [savingMessage, setSavingMessage] = useState<string>('');

// savingDescription is a JSX element that will be displayed in the serverError bar on the SaveChangesPanel. This allows us to provide more information on loading while previous changes are applied
const [savingDescription, setSavingDescription] = useState<JSX.Element | null>(null);

const savingButtonMessages = {
SAVING_PREVIOUS_CHANGE: formatMessage({id: 'admin.ip_filtering.saving_previous_change', defaultMessage: 'Other changes being applied...'}),
SAVING_CHANGES: formatMessage({id: 'admin.ip_filtering.saving_changes', defaultMessage: 'Applying changes...'}),
};

const savingDescriptionMessages = {
SAVING_PREVIOUS_CHANGE: formatMessage({id: 'admin.ip_filtering.saving_previous_change_description', defaultMessage: 'Please wait while changes from another admin are applied.'}),
SAVING_CHANGES: formatMessage({id: 'admin.ip_filtering.saving_changes_description', defaultMessage: 'Please wait while your changes are applied.'}),
};

useEffect(() => {
getInstallationStatus();

getIPFilters((data: AllowedIPRange[]) => {
setIpFilters(data);
setOriginalIpFilters(data);
Expand All @@ -57,7 +79,7 @@ const IPFiltering = () => {
setSaveNeeded(haveFiltersChanged);
}, [ipFilters, originalIpFilters]);

const currentIPIsInRange = () => {
const currentIPIsInRange = (): boolean => {
if (!filterToggle) {
return true;
}
Expand Down Expand Up @@ -92,6 +114,61 @@ const IPFiltering = () => {
}
}, [filterToggle]);

function pollInstallationStatus() {
let installationFetchAttempts = 0;
const interval = setInterval(async () => {
if (installationFetchAttempts > 15) {
// Average time for provisioner to update is around 30 seconds. This allows up to 75 seconds before it will stop fetching, displaying an error
setSavingDescription((
<>
<AlertOutlineIcon size={16}/> {formatMessage({id: 'admin.ip_filtering.failed_to_fetch_installation_state', defaultMessage: 'Failed to fetch your workspace\'s status. Please try again later or contact support.'})}
</>
));
clearInterval(interval);
return;
}
const result = await dispatch(getInstallation());
installationFetchAttempts++;
if (result.data) {
const {data} = result;
if (data.state === 'stable') {
setSaving(false);
setSavingDescription(null);
clearInterval(interval);
}
setInstallationStatus(data.state);
}
}, 5000);
}

async function getInstallationStatus() {
const result = await dispatch(getInstallation());
if (result.data) {
const {data} = result;
setInstallationStatus(data.state);
if (installationStatus === '' && data.state !== 'stable') {
// This is the first load of the page, and the installation is not stable, so we must lock saving until it becomes stable
setSaving(true);

// Override the default messages for the save button and the error message to be communicative of the current state to the user
setSavingMessage(savingButtonMessages.SAVING_PREVIOUS_CHANGE);
changeSavingDescription(savingDescriptionMessages.SAVING_PREVIOUS_CHANGE);
}
if (data.state !== 'stable') {
pollInstallationStatus();
}
}
}

function changeSavingDescription(text: string) {
setSavingDescription((
<div className='saving-message-description'>
{text}
</div>
),
);
}

function handleEditFilter(filter: AllowedIPRange, existingRange?: AllowedIPRange) {
setIpFilters((prevIpFilters) => {
if (!prevIpFilters) {
Expand Down Expand Up @@ -155,13 +232,16 @@ const IPFiltering = () => {
}

function handleSave() {
setInstallationStatus('update-requested');
setSaving(true);
setSavingMessage(savingButtonMessages.SAVING_CHANGES);
changeSavingDescription(savingDescriptionMessages.SAVING_CHANGES);
dispatch(closeModal(ModalIdentifiers.IP_FILTERING_SAVE_CONFIRMATION_MODAL));

const success = (data: AllowedIPRange[]) => {
setIpFilters(data);
setSaving(false);
setSaveNeeded(false);
setOriginalIpFilters(data);
getInstallationStatus();
};

applyIPFilters(ipFilters ?? [], success);
Expand Down Expand Up @@ -220,6 +300,10 @@ const IPFiltering = () => {
}

const saveBarError = () => {
if (savingDescription !== null) {
return savingDescription;
}

if (currentIPIsInRange()) {
return undefined;
}
Expand Down Expand Up @@ -256,10 +340,11 @@ const IPFiltering = () => {
</div>
<SaveChangesPanel
saving={saving}
saveNeeded={saveNeeded}
isDisabled={!currentIPIsInRange}
saveNeeded={saveNeeded || installationStatus !== 'stable'}
isDisabled={!currentIPIsInRange() || installationStatus !== 'stable'}
onClick={handleSaveClick}
serverError={saveBarError()}
savingMessage={savingMessage}
cancelLink=''
/>
</div>
Expand Down
Loading

0 comments on commit 95670ab

Please sign in to comment.