diff --git a/ui/src/__tests__/spec/tests/domain.spec.js b/ui/src/__tests__/spec/tests/domain.spec.js
index 5ee604aad00..198f4e42c29 100644
--- a/ui/src/__tests__/spec/tests/domain.spec.js
+++ b/ui/src/__tests__/spec/tests/domain.spec.js
@@ -62,6 +62,39 @@ describe('Domain', () => {
);
});
+ it('should successfully add and clear domain slack channel', async () => {
+ await browser.newUser();
+ await browser.url(`/`);
+ await expect(browser).toHaveUrl(expect.stringContaining('athenz'));
+
+ let testDomain = await $('a*=athenz.dev.functional-test');
+ await browser.waitUntil(async () => await testDomain.isClickable());
+ await testDomain.click();
+
+ // expand domain details
+ let expand = await $(
+ `.//*[local-name()="svg" and @data-wdio="domain-details-expand-icon"]`
+ );
+ await expand.click();
+
+ // click add business service
+ let addSlackChannel = await $('a[data-testid="add-slack-channel"]');
+ await browser.waitUntil(
+ async () => await addSlackChannel.isClickable()
+ );
+ await addSlackChannel.click();
+
+ let randomInt = Math.floor(Math.random() * 100); // random number to append to slack channel name
+ let slackChannelName = 'slack-channel-' + randomInt;
+ let slackChannelInput = await $('input[name="slack-channel-input"]');
+ await slackChannelInput.addValue(slackChannelName);
+ let submitButton = await $('button*=Submit');
+ await submitButton.click();
+ await expect(addSlackChannel).toHaveText(
+ expect.stringContaining(slackChannelName)
+ );
+ });
+
it(TEST_ADD_BUSINESS_SERVICE_INPUT_PRESERVES_CONTENTS_ON_BLUR, async () => {
currentTest =
TEST_ADD_BUSINESS_SERVICE_INPUT_PRESERVES_CONTENTS_ON_BLUR;
diff --git a/ui/src/components/header/DomainDetails.js b/ui/src/components/header/DomainDetails.js
index 9823ea2d710..161779bad5d 100644
--- a/ui/src/components/header/DomainDetails.js
+++ b/ui/src/components/header/DomainDetails.js
@@ -47,6 +47,7 @@ import Icon from '../denali/icons/Icon';
import AddPoc from '../member/AddPoc';
import { selectAllUsers } from '../../redux/selectors/user';
import AddEnvironmentModal from '../modal/AddEnvironmentModal';
+import AddSlackChannelModal from '../modal/AddSlackChannelModal';
const DomainSectionDiv = styled.div`
margin: 20px 0;
@@ -127,6 +128,7 @@ class DomainDetails extends React.Component {
),
expandedDomain: false,
environmentName: this.props.domainDetails.environment || 'add',
+ slackChannel: this.props.domainDetails.slackChannel || 'add',
};
this.showError = this.showError.bind(this);
this.closeModal = this.closeModal.bind(this);
@@ -197,6 +199,41 @@ class DomainDetails extends React.Component {
}
}
+ onClickSlackChannel() {
+ this.setState({
+ showSlackChannelModal: true,
+ });
+ }
+
+ onSlackChannelUpdateSuccessCb(slackChannelName) {
+ let newState = {
+ showSlackChannelModal: false,
+ showSuccess: true,
+ };
+
+ if (!!!slackChannelName) {
+ slackChannelName = 'add';
+ }
+ newState.slackChannel = slackChannelName;
+ newState.successMessage = 'Successfully updated Slack channel';
+ this.setState(newState);
+ setTimeout(
+ () =>
+ this.setState({
+ showSuccess: false,
+ }),
+ MODAL_TIME_OUT + 1000
+ );
+ }
+
+ onClickSlackChannelCancel() {
+ this.setState({
+ showSlackChannelModal: false,
+ errorMessage: null,
+ errorMessageForModal: '',
+ });
+ }
+
onClickPointOfContactCancel() {
this.setState({
showPoc: false,
@@ -505,6 +542,7 @@ class DomainDetails extends React.Component {
this.state.poc,
'security-owner'
);
+ let onClickSlackChannel = this.onClickSlackChannel.bind(this);
let contactType;
let pocName;
let openPocModal;
@@ -553,6 +591,24 @@ class DomainDetails extends React.Component {
''
);
+ let slackChannelModal = this.state.showSlackChannelModal ? (
+
+ ) : (
+ ''
+ );
+
return (
@@ -675,6 +731,7 @@ class DomainDetails extends React.Component {
/>
) : null}
{environmentModal}
+ {slackChannelModal}
{this.state.expandedDomain ? (
@@ -735,6 +792,21 @@ class DomainDetails extends React.Component {
ENVIRONMENT
+
+
+
+ {this.state.slackChannel}
+
+
+ SLACK CHANNEL
+
) : null}
diff --git a/ui/src/components/modal/AddSlackChannelModal.js b/ui/src/components/modal/AddSlackChannelModal.js
new file mode 100644
index 00000000000..23b4bf79344
--- /dev/null
+++ b/ui/src/components/modal/AddSlackChannelModal.js
@@ -0,0 +1,177 @@
+/*
+ * Copyright The Athenz Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import React from 'react';
+import { connect } from 'react-redux';
+import AddModal from '../modal/AddModal';
+import styled from '@emotion/styled';
+import InputLabel from '../denali/InputLabel';
+import { colors } from '../denali/styles';
+import { selectAllUsers } from '../../redux/selectors/user';
+import RequestUtils from '../utils/RequestUtils';
+import Input from '../denali/Input';
+import RegexUtils from '../utils/RegexUtils';
+
+const SectionsDiv = styled.div`
+ width: 760px;
+ text-align: left;
+ background-color: ${colors.white};
+`;
+
+const SectionDiv = styled.div`
+ align-items: flex-start;
+ display: flex;
+ flex-flow: row nowrap;
+ padding: 10px 30px;
+`;
+
+const ContentDiv = styled.div`
+ flex: 1 1;
+ display: flex;
+ flex-flow: row nowrap;
+`;
+
+const StyledInputLabel = styled(InputLabel)`
+ flex-basis: 32%;
+ font-weight: 600;
+ line-height: 36px;
+`;
+
+const StyledInput = styled(Input)`
+ flex-basis: 80%;
+`;
+
+class AddSlackChannelModal extends React.Component {
+ constructor(props) {
+ super(props);
+ this.api = props.api;
+ this.onSubmit = this.onSubmit.bind(this);
+ let slackChannelName =
+ this.props.slackChannelName === 'add'
+ ? ''
+ : this.props.slackChannelName;
+ this.state = {
+ showModal: this.props.isOpen,
+ slackChannelName: slackChannelName,
+ };
+ }
+
+ onSubmit() {
+ if (!!this.state.slackChannelName) {
+ if (
+ !RegexUtils.validate(
+ this.state.slackChannelName,
+ /^[a-z0-9-_]{1}[a-z0-9-_]{0,79}$/
+ )
+ ) {
+ this.setState({
+ errorMessage:
+ 'Slack channel name is invalid. It should be between 1-80 characters long and can only contain lowercase letters, numbers, hyphens and underscores.',
+ });
+ return;
+ }
+ }
+
+ let meta = {
+ slackChannel: this.state.slackChannelName,
+ };
+ let domainName = this.props.domain;
+ let auditMsg = `Updating Slack channel ${this.state.slackChannelName} for domain ${this.props.domain}`;
+ let csrf = this.props.csrf;
+ this.api
+ .putMeta(domainName, domainName, meta, auditMsg, csrf, 'domain')
+ .then(() => {
+ this.setState({
+ showModal: false,
+ });
+ this.props.onSlackChannelUpdateSuccessCb(
+ this.state.slackChannelName
+ );
+ })
+ .catch((err) => {
+ this.setState({
+ errorMessage: RequestUtils.xhrErrorCheckHelper(err),
+ });
+ });
+ }
+
+ inputChanged(key, evt) {
+ let value = '';
+ if (evt.target) {
+ value = evt.target.value;
+ } else {
+ value = evt ? evt : '';
+ }
+ this.setState({ [key]: value });
+ }
+
+ render() {
+ let memberLabel = 'Slack Channel Name';
+ let title =
+ 'Update slack channel to receive notifications for ' +
+ this.props.domain;
+ let sections = (
+
+
+
+ {memberLabel}
+
+
+
+
+
+
+ );
+
+ return (
+
+ );
+ }
+}
+
+const mapStateToProps = (state, props) => {
+ return {
+ ...props,
+ userList: selectAllUsers(state),
+ };
+};
+const mapDispatchToProps = (dispatch) => ({});
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(AddSlackChannelModal);
diff --git a/ui/src/config/msd.json b/ui/src/config/msd.json
index 605046e6ccc..73bb965375c 100644
--- a/ui/src/config/msd.json
+++ b/ui/src/config/msd.json
@@ -3531,4 +3531,4 @@
"expected": "OK"
}
]
-}
\ No newline at end of file
+}
diff --git a/ui/src/config/ums.json b/ui/src/config/ums.json
index 737aa3daace..36a5a4b1297 100644
--- a/ui/src/config/ums.json
+++ b/ui/src/config/ums.json
@@ -2783,4 +2783,4 @@
}
}
]
-}
\ No newline at end of file
+}
diff --git a/ui/src/config/zms.json b/ui/src/config/zms.json
index 1dba066077d..09985b45057 100644
--- a/ui/src/config/zms.json
+++ b/ui/src/config/zms.json
@@ -12348,4 +12348,4 @@
"expected": "OK"
}
]
-}
\ No newline at end of file
+}
diff --git a/ui/src/config/zts.json b/ui/src/config/zts.json
index cf9c2fb59b5..2950a759ec9 100644
--- a/ui/src/config/zts.json
+++ b/ui/src/config/zts.json
@@ -4551,4 +4551,4 @@
"expected": "OK"
}
]
-}
\ No newline at end of file
+}
diff --git a/ui/src/server/constants.js b/ui/src/server/constants.js
new file mode 100644
index 00000000000..5b400eedc5d
--- /dev/null
+++ b/ui/src/server/constants.js
@@ -0,0 +1,21 @@
+/*
+ * Copyright The Athenz Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const validationPatterns = {
+ domainSlackChannel: /^[a-z0-9-_]{1}[a-z0-9-_]{0,79}$/,
+};
+
+module.exports = validationPatterns;
diff --git a/ui/src/server/handlers/api.js b/ui/src/server/handlers/api.js
index 6525148f6c0..f7d69bbaee9 100644
--- a/ui/src/server/handlers/api.js
+++ b/ui/src/server/handlers/api.js
@@ -22,6 +22,7 @@ const apiUtils = require('../utils/apiUtils');
const debug = require('debug')('AthenzUI:server:handlers:api');
const cytoscape = require('cytoscape');
let dagre = require('cytoscape-dagre');
+const validationRegex = require('../constants');
let appConfig = {};
@@ -51,6 +52,22 @@ const responseHandler = function (err, data) {
}
};
+const validateFields = (value, field) => {
+ let regex = validationRegex.hasOwnProperty(field)
+ ? validationRegex[field]
+ : null;
+ if (!regex) {
+ console.log('Missing regex for ' + field);
+ }
+
+ if (regex.test(value)) {
+ return true;
+ }
+
+ console.log('Validation error for ' + field + ': ' + value);
+ return false;
+};
+
const deleteInstanceZts = (
provider,
domainName,
@@ -1501,18 +1518,40 @@ Fetchr.registerService({
break;
}
case 'domain': {
- req.clients.zms.putDomainMeta(
- {
- name: params.domainName,
- auditRef: params.auditRef,
- detail: params.detail,
- },
- responseHandler.bind({
- caller: 'putDomainMeta',
- callback,
- req,
- })
- );
+ if (
+ params.detail.slackChannel &&
+ !validateFields(
+ params.detail.slackChannel,
+ 'domainSlackChannel'
+ )
+ ) {
+ let validationErr = {
+ status: '400',
+ message: {
+ message: 'Invalid Slack Channel format',
+ },
+ };
+ callback(
+ errorHandler.fetcherError(
+ validationErr,
+ 'validationError'
+ )
+ );
+ } else {
+ req.clients.zms.putDomainMeta(
+ {
+ name: params.domainName,
+ auditRef: params.auditRef,
+ detail: params.detail,
+ },
+ responseHandler.bind({
+ caller: 'putDomainMeta',
+ callback,
+ req,
+ })
+ );
+ }
+
break;
}
case 'policy': {