User Permissions - initial implementation (#1046)

* initial checkin of component permissions

* initial implementaiton of user permissions for devices, device groups, rules, jobs, and alarms

* review feedback

* fix nit in readme
This commit is contained in:
Mary Ellen Chaffin 2018-07-18 14:14:43 -07:00 коммит произвёл Stephen Pryor
Родитель 3a4c5bb933
Коммит 5f784514e0
25 изменённых файлов: 525 добавлений и 279 удалений

Просмотреть файл

@ -28,6 +28,9 @@
"retryFailure": "Oops, we got a temporary error from the service but were unable to recover. Try again later.",
"unknown": "An unknown error occurred. {{message}}"
},
"protected": {
"permissionDenied": "Permission {{permission}} is denied."
},
"settingsFlyout": {
"title": "System settings",
"version": "Version {{version}}",

Просмотреть файл

@ -3,7 +3,8 @@
import React from 'react';
import { IoTHubManagerService } from 'services';
import { Btn } from 'components/shared';
import { permissions } from 'services/models';
import { Btn, Protected } from 'components/shared';
import { svgs, LinkedComponent } from 'utilities';
import Flyout from 'components/shared/flyout';
import DeviceGroupForm from './views/deviceGroupForm';
@ -69,7 +70,9 @@ export class ManageDeviceGroups extends LinkedComponent {
this.state.addNewDeviceGroup || !!this.state.selectedDeviceGroup
? <DeviceGroupForm {...this.props} {...this.state} cancel={this.closeForm} />
: <div>
<Btn className="add-btn" svg={svgs.plus} onClick={this.toggleNewFilter}>{t('deviceGroupsFlyout.create')}</Btn>
<Protected permission={permissions.createDeviceGroups}>
<Btn className="add-btn" svg={svgs.plus} onClick={this.toggleNewFilter}>{t('deviceGroupsFlyout.create')}</Btn>
</Protected>
{deviceGroups.length > 0 && <DeviceGroups {...this.props} onEditDeviceGroup={this.onEditDeviceGroup} />}
</div>
}

Просмотреть файл

@ -2,6 +2,7 @@
import React from 'react';
import { permissions } from 'services/models';
import { svgs, LinkedComponent, Validator } from 'utilities';
import {
AjaxError,
@ -10,7 +11,8 @@ import {
FormControl,
FormGroup,
FormLabel,
Indicator
Indicator,
Protected
} from 'components/shared';
import { ConfigService } from 'services';
import {
@ -266,21 +268,25 @@ class DeviceGroupForm extends LinkedComponent {
}
{ this.state.isPending && <Indicator pattern="bar" size="medium" />}
<BtnToolbar>
<Btn
primary
disabled={!this.formIsValid() || conditionsHaveErrors || this.state.isPending}
type="submit">
{t('deviceGroupsFlyout.save')}
</Btn>
<Protected permission={permissions.updateDeviceGroups}>
<Btn
primary
disabled={!this.formIsValid() || conditionsHaveErrors || this.state.isPending}
type="submit">
{t('deviceGroupsFlyout.save')}
</Btn>
</Protected>
<Btn svg={svgs.cancelX} onClick={this.props.cancel}>{t('deviceGroupsFlyout.cancel')}</Btn>
{
// Don't show delete btn if it is a new group or the group is currently active
this.state.isEdit &&
<Btn svg={svgs.trash}
onClick={this.deleteDeviceGroup}
disabled={this.props.activeDeviceGroupId === this.state.id || this.state.isPending}>
{t('deviceGroupsFlyout.conditions.delete')}
</Btn>
<Protected permission={permissions.deleteDeviceGroups}>
<Btn svg={svgs.trash}
onClick={this.deleteDeviceGroup}
disabled={this.props.activeDeviceGroupId === this.state.id || this.state.isPending}>
{t('deviceGroupsFlyout.conditions.delete')}
</Btn>
</Protected>
}
</BtnToolbar>
{ this.state.error && <AjaxError t={t} error={this.state.error} /> }

Просмотреть файл

@ -1,10 +1,20 @@
// Copyright (c) Microsoft. All rights reserved.
import React, { Component } from 'react';
import { permissions } from 'services/models';
import { DevicesGrid } from './devicesGrid';
import { DeviceGroupDropdownContainer as DeviceGroupDropdown } from 'components/app/deviceGroupDropdown';
import { ManageDeviceGroupsBtnContainer as ManageDeviceGroupsBtn } from 'components/app/manageDeviceGroupsBtn';
import { AjaxError, Btn, RefreshBar, PageContent, ContextMenu, SearchInput } from 'components/shared';
import {
AjaxError,
Btn,
ContextMenu,
PageContent,
Protected,
RefreshBar,
SearchInput
} from 'components/shared';
import { DeviceNewContainer } from './flyouts/deviceNew';
import { SIMManagementContainer } from './flyouts/SIMManagement';
import { svgs } from 'utilities';
@ -64,9 +74,15 @@ export class Devices extends Component {
<DeviceGroupDropdown />
<SearchInput onChange={this.searchOnChange} placeholder={t('devices.searchPlaceholder')} />
{ this.state.contextBtns }
<Btn svg={svgs.simmanagement} onClick={this.openSIMManagement}>{t('devices.flyouts.SIMManagement.title')}</Btn>
<Btn svg={svgs.plus} onClick={this.openNewDeviceFlyout}>{t('devices.flyouts.new.contextMenuName')}</Btn>
<ManageDeviceGroupsBtn />
<Protected permission={permissions.updateSIMManagement}>
<Btn svg={svgs.simmanagement} onClick={this.openSIMManagement}>{t('devices.flyouts.SIMManagement.title')}</Btn>
</Protected>
<Protected permission={permissions.createDevices}>
<Btn svg={svgs.plus} onClick={this.openNewDeviceFlyout}>{t('devices.flyouts.new.contextMenuName')}</Btn>
</Protected>
<Protected permission={permissions.updateDeviceGroups}>
<ManageDeviceGroupsBtn />
</Protected>
</ContextMenu>,
<PageContent className="devices-container" key="page-content">
<RefreshBar refresh={fetchDevices} time={lastUpdated} isPending={isPending} t={t} />

Просмотреть файл

@ -1,7 +1,8 @@
// Copyright (c) Microsoft. All rights reserved.
import React, { Component } from 'react';
import { Btn, PcsGrid } from 'components/shared';
import { permissions } from 'services/models';
import { Btn, PcsGrid, Protected } from 'components/shared';
import { deviceColumnDefs, defaultDeviceGridProps } from './devicesGridConfig';
import { DeviceDeleteContainer } from '../flyouts/deviceDelete';
import { DeviceJobsContainer } from '../flyouts/deviceJobs';
@ -39,8 +40,12 @@ export class DevicesGrid extends Component {
];
this.contextBtns = [
<Btn key="jobs" svg={svgs.reconfigure} onClick={this.openFlyout('jobs')}>{props.t('devices.flyouts.jobs.title')}</Btn>,
<Btn key="delete" svg={svgs.trash} onClick={this.openFlyout('delete')}>{props.t('devices.flyouts.delete.title')}</Btn>
<Protected permission={permissions.createJobs}>
<Btn key="jobs" svg={svgs.reconfigure} onClick={this.openFlyout('jobs')}>{props.t('devices.flyouts.jobs.title')}</Btn>
</Protected>,
<Protected permission={permissions.deleteDevices}>
<Btn key="delete" svg={svgs.trash} onClick={this.openFlyout('delete')}>{props.t('devices.flyouts.delete.title')}</Btn>
</Protected>
];
}

Просмотреть файл

@ -4,8 +4,9 @@ import React from 'react';
import { Trans } from 'react-i18next';
import { Link } from 'react-router-dom'
import { permissions } from 'services/models';
import { LinkedComponent } from 'utilities';
import { FormControl } from 'components/shared';
import { FormControl, Protected } from 'components/shared';
import Flyout from 'components/shared/flyout';
import './SIMManagement.css';
@ -47,33 +48,35 @@ export class SIMManagement extends LinkedComponent {
<Flyout.CloseBtn onClick={onClose} />
</Flyout.Header>
<Flyout.Content className="sim-management-container">
<div className="sim-management-selector">
<div className="sim-management-label-selector">{t(`devices.flyouts.SIMManagement.provider`)}</div>
<div className="sim-management-dropdown">
<FormControl
type="select"
className="sim-management-dropdown"
options={options}
searchable={false}
clearable={false}
placeholder={t('devices.flyouts.SIMManagement.select')}
link={this.providerLink} />
<Protected permission={permissions.updateSIMManagement}>
<div className="sim-management-selector">
<div className="sim-management-label-selector">{t(`devices.flyouts.SIMManagement.provider`)}</div>
<div className="sim-management-dropdown">
<FormControl
type="select"
className="sim-management-dropdown"
options={options}
searchable={false}
clearable={false}
placeholder={t('devices.flyouts.SIMManagement.select')}
link={this.providerLink} />
</div>
</div>
</div>
{
!!provider &&
<Section.Container className="hide-border" collapsable={false}>
<Section.Header>{t(`devices.flyouts.SIMManagement.summaryHeader`)}</Section.Header>
<Section.Content>
<div>{t(`devices.flyouts.SIMManagement.header.${provider}`)}</div>
<div className="sim-management-label-desctiption">
<Trans i18nKey={`devices.flyouts.SIMManagement.description.${provider}`}>
Feature is... <Link to={simManagementUrl} target="_blank">{t(`devices.flyouts.SIMManagement.here`)}</Link> ...your account.
</Trans>
</div>
</Section.Content>
</Section.Container>
}
{
!!provider &&
<Section.Container className="hide-border" collapsable={false}>
<Section.Header>{t(`devices.flyouts.SIMManagement.summaryHeader`)}</Section.Header>
<Section.Content>
<div>{t(`devices.flyouts.SIMManagement.header.${provider}`)}</div>
<div className="sim-management-label-desctiption">
<Trans i18nKey={`devices.flyouts.SIMManagement.description.${provider}`}>
Feature is... <Link to={simManagementUrl} target="_blank">{t(`devices.flyouts.SIMManagement.here`)}</Link> ...your account.
</Trans>
</div>
</Section.Content>
</Section.Container>
}
</Protected>
</Flyout.Content>
</Flyout.Container>
);

Просмотреть файл

@ -6,6 +6,7 @@ import update from 'immutability-helper';
import { IoTHubManagerService } from 'services';
import { svgs } from 'utilities';
import { permissions } from 'services/models';
import {
AjaxError,
Btn,
@ -16,6 +17,7 @@ import {
FlyoutCloseBtn,
FlyoutContent,
Indicator,
Protected,
SectionDesc,
SectionHeader,
SummaryBody,
@ -131,47 +133,49 @@ export class DeviceDelete extends Component {
<FlyoutCloseBtn onClick={onClose} />
</FlyoutHeader>
<FlyoutContent>
<form className="device-delete-container" onSubmit={this.deleteDevices}>
<div className="device-delete-header">{t('devices.flyouts.delete.header')}</div>
<div className="device-delete-descr">{t('devices.flyouts.delete.description')}</div>
<ToggleBtn
value={confirmStatus}
onChange={this.toggleConfirm}>
{confirmStatus ? t('devices.flyouts.delete.confirmYes') : t('devices.flyouts.delete.confirmNo')}
</ToggleBtn>
{
containsSimulatedDevices &&
<div className="simulated-device-selected">
<Svg path={svgs.infoBubble} className="info-icon" />
{t('devices.flyouts.delete.simulatedNotSupported')}
</div>
}
<Protected permission={permissions.deleteDevices}>
<form className="device-delete-container" onSubmit={this.deleteDevices}>
<div className="device-delete-header">{t('devices.flyouts.delete.header')}</div>
<div className="device-delete-descr">{t('devices.flyouts.delete.description')}</div>
<ToggleBtn
value={confirmStatus}
onChange={this.toggleConfirm}>
{confirmStatus ? t('devices.flyouts.delete.confirmYes') : t('devices.flyouts.delete.confirmNo')}
</ToggleBtn>
{
containsSimulatedDevices &&
<div className="simulated-device-selected">
<Svg path={svgs.infoBubble} className="info-icon" />
{t('devices.flyouts.delete.simulatedNotSupported')}
</div>
}
<SummarySection>
<SectionHeader>{t('devices.flyouts.delete.summaryHeader')}</SectionHeader>
<SummaryBody>
<SummaryCount>{summaryCount}</SummaryCount>
<SectionDesc>{summaryMessage}</SectionDesc>
{this.state.isPending && <Indicator />}
{completedSuccessfully && <Svg className="summary-icon" path={svgs.apply} />}
</SummaryBody>
</SummarySection>
<SummarySection>
<SectionHeader>{t('devices.flyouts.delete.summaryHeader')}</SectionHeader>
<SummaryBody>
<SummaryCount>{summaryCount}</SummaryCount>
<SectionDesc>{summaryMessage}</SectionDesc>
{this.state.isPending && <Indicator />}
{completedSuccessfully && <Svg className="summary-icon" path={svgs.apply} />}
</SummaryBody>
</SummarySection>
{error && <AjaxError className="device-delete-error" t={t} error={error} />}
{
!changesApplied &&
<BtnToolbar>
<Btn svg={svgs.trash} primary={true} disabled={isPending || physicalDevices.length === 0 || !confirmStatus} type="submit">{t('devices.flyouts.delete.apply')}</Btn>
<Btn svg={svgs.cancelX} onClick={onClose}>{t('devices.flyouts.delete.cancel')}</Btn>
</BtnToolbar>
}
{
!!changesApplied &&
<BtnToolbar>
<Btn svg={svgs.cancelX} onClick={onClose}>{t('devices.flyouts.delete.close')}</Btn>
</BtnToolbar>
}
</form>
{error && <AjaxError className="device-delete-error" t={t} error={error} />}
{
!changesApplied &&
<BtnToolbar>
<Btn svg={svgs.trash} primary={true} disabled={isPending || physicalDevices.length === 0 || !confirmStatus} type="submit">{t('devices.flyouts.delete.apply')}</Btn>
<Btn svg={svgs.cancelX} onClick={onClose}>{t('devices.flyouts.delete.cancel')}</Btn>
</BtnToolbar>
}
{
!!changesApplied &&
<BtnToolbar>
<Btn svg={svgs.cancelX} onClick={onClose}>{t('devices.flyouts.delete.close')}</Btn>
</BtnToolbar>
}
</form>
</Protected>
</FlyoutContent>
</Flyout>
);

Просмотреть файл

@ -3,7 +3,7 @@
import React from 'react';
import { LinkedComponent } from 'utilities';
import { permissions } from 'services/models';
import {
Flyout,
FlyoutHeader,
@ -13,6 +13,7 @@ import {
ErrorMsg,
FormGroup,
FormLabel,
Protected,
Radio
} from 'components/shared';
import {
@ -64,37 +65,39 @@ export class DeviceJobs extends LinkedComponent {
<FlyoutCloseBtn onClick={onClose} />
</FlyoutHeader>
<FlyoutContent>
<div className="device-jobs-container">
{
devices.length === 0 &&
<ErrorMsg className="device-jobs-error">{t("devices.flyouts.jobs.noDevices")}</ErrorMsg>
}
{
devices.length > 0 && [
<FormGroup key="job-selection">
<FormLabel>{t('devices.flyouts.jobs.selectJob')}</FormLabel>
<Radio link={this.jobTypeLink} value="tags">
{t('devices.flyouts.jobs.tags.radioLabel')}
</Radio>
<Radio link={this.jobTypeLink} value="methods">
{t('devices.flyouts.jobs.methods.radioLabel')}
</Radio>
<Radio link={this.jobTypeLink} value="properties">
{t('devices.flyouts.jobs.properties.radioLabel')}
</Radio>
</FormGroup>,
this.jobTypeLink.value === 'tags'
? <DeviceJobTags key="job-details" t={t} onClose={onClose} devices={devices} updateTags={updateTags} />
: null,
this.jobTypeLink.value === 'methods'
? <DeviceJobMethods key="job-details" t={t} onClose={onClose} devices={devices} />
: null,
this.jobTypeLink.value === 'properties'
? <DeviceJobProperties key="job-details" t={t} onClose={onClose} devices={devices} updateProperties={updateProperties} />
: null
]
}
</div>
<Protected permission={permissions.createJobs}>
<div className="device-jobs-container">
{
devices.length === 0 &&
<ErrorMsg className="device-jobs-error">{t("devices.flyouts.jobs.noDevices")}</ErrorMsg>
}
{
devices.length > 0 && [
<FormGroup key="job-selection">
<FormLabel>{t('devices.flyouts.jobs.selectJob')}</FormLabel>
<Radio link={this.jobTypeLink} value="tags">
{t('devices.flyouts.jobs.tags.radioLabel')}
</Radio>
<Radio link={this.jobTypeLink} value="methods">
{t('devices.flyouts.jobs.methods.radioLabel')}
</Radio>
<Radio link={this.jobTypeLink} value="properties">
{t('devices.flyouts.jobs.properties.radioLabel')}
</Radio>
</FormGroup>,
this.jobTypeLink.value === 'tags'
? <DeviceJobTags key="job-details" t={t} onClose={onClose} devices={devices} updateTags={updateTags} />
: null,
this.jobTypeLink.value === 'methods'
? <DeviceJobMethods key="job-details" t={t} onClose={onClose} devices={devices} />
: null,
this.jobTypeLink.value === 'properties'
? <DeviceJobProperties key="job-details" t={t} onClose={onClose} devices={devices} updateProperties={updateProperties} />
: null
]
}
</div>
</Protected>
</FlyoutContent>
</Flyout>
);

Просмотреть файл

@ -4,7 +4,7 @@ import React from 'react';
import update from 'immutability-helper';
import { DeviceSimulationService, IoTHubManagerService } from 'services';
import { authenticationTypeOptions, toNewDeviceRequestModel } from 'services/models';
import { authenticationTypeOptions, permissions, toNewDeviceRequestModel } from 'services/models';
import {
copyToClipboard,
int,
@ -28,6 +28,7 @@ import {
FormLabel,
FormSection,
Indicator,
Protected,
Radio,
SectionDesc,
SectionHeader,
@ -330,107 +331,109 @@ export class DeviceNew extends LinkedComponent {
<FlyoutCloseBtn onClick={onClose} />
</FlyoutHeader>
<FlyoutContent>
<form className="devices-new-container" onSubmit={this.apply}>
<div className="devices-new-content">
<FormGroup>
<FormLabel>{t(deviceTypeOptions.labelName)}</FormLabel>
<Radio link={this.deviceTypeLink} value={deviceTypeOptions.simulated.value} onChange={this.formControlChange}>
{t(deviceTypeOptions.simulated.labelName)}
</Radio>
<Radio link={this.deviceTypeLink} value={deviceTypeOptions.physical.value} onChange={this.formControlChange}>
{t(deviceTypeOptions.physical.labelName)}
</Radio>
</FormGroup>
{
isSimulatedDevice && [
<FormGroup key="deviceCount">
<FormLabel>{t('devices.flyouts.new.count.label')}</FormLabel>
<FormControl link={this.countLink} type="text" onChange={this.formControlChange} />
</FormGroup>,
<FormGroup key="deviceId">
<FormLabel>{t('devices.flyouts.new.deviceIdExample.label')}</FormLabel>
<div className="device-id-example">{t('devices.flyouts.new.deviceIdExample.format', { deviceName })}</div>
</FormGroup>,
<FormGroup key="deviceModel">
<FormLabel>{t('devices.flyouts.new.deviceModel.label')}</FormLabel>
<FormControl link={this.deviceModelLink} type="select" options={deviceModelOptions} placeholder={t('devices.flyouts.new.deviceModel.hint')} onChange={this.formControlChange} />
</FormGroup>
]
}
{
!isSimulatedDevice && [
<FormGroup key="deviceCount">
<FormLabel>{t('devices.flyouts.new.count.label')}</FormLabel>
<div className="device-count">{this.countLink.value}</div>
</FormGroup>,
<FormGroup key="deviceId">
<FormLabel>{t('devices.flyouts.new.deviceId.label')}</FormLabel>
<Radio link={this.isGenerateIdLink} value={deviceIdTypeOptions.manual.value} onChange={this.formControlChange}>
<FormControl className="device-id" link={this.deviceIdLink} disabled={isGenerateId} type="text" placeholder={t(deviceIdTypeOptions.manual.hintName)} onChange={this.formControlChange} />
</Radio>
<Radio link={this.isGenerateIdLink} value={deviceIdTypeOptions.generate.value} onChange={this.formControlChange}>
{t(deviceIdTypeOptions.generate.labelName)}
</Radio>
</FormGroup>,
<FormGroup key="authType">
<FormLabel>{t(authTypeOptions.labelName)}</FormLabel>
<Radio link={this.authenticationTypeLink} value={authTypeOptions.symmetric.value} onChange={this.formControlChange}>
{t(authTypeOptions.symmetric.labelName)}
</Radio>
<Radio link={this.authenticationTypeLink} value={authTypeOptions.x509.value} onChange={this.formControlChange}>
{t(authTypeOptions.x509.labelName)}
</Radio>
</FormGroup>,
<FormGroup key="authKeyType">
<FormLabel>{t(authKeyTypeOptions.labelName)}</FormLabel>
<Radio link={this.isGenerateKeysLink} value={authKeyTypeOptions.generate.value} disabled={isX509} onChange={this.formControlChange}>
{t(authKeyTypeOptions.generate.labelName)}
</Radio>
<Radio link={this.isGenerateKeysLink} value={authKeyTypeOptions.manual.value} onChange={this.formControlChange}>
{t(authKeyTypeOptions.manual.labelName)}
</Radio>
<FormGroup className="sub-settings">
<FormLabel>{isX509 ? t('devices.flyouts.new.authenticationKey.primaryThumbprint') : t('devices.flyouts.new.authenticationKey.primaryKey')}</FormLabel>
<FormControl link={this.primaryKeyLink} disabled={isGenerateKeys} type="text" placeholder={t('devices.flyouts.new.authenticationKey.hint')} onChange={this.formControlChange} />
<Protected permission={permissions.createDevices}>
<form className="devices-new-container" onSubmit={this.apply}>
<div className="devices-new-content">
<FormGroup>
<FormLabel>{t(deviceTypeOptions.labelName)}</FormLabel>
<Radio link={this.deviceTypeLink} value={deviceTypeOptions.simulated.value} onChange={this.formControlChange}>
{t(deviceTypeOptions.simulated.labelName)}
</Radio>
<Radio link={this.deviceTypeLink} value={deviceTypeOptions.physical.value} onChange={this.formControlChange}>
{t(deviceTypeOptions.physical.labelName)}
</Radio>
</FormGroup>
{
isSimulatedDevice && [
<FormGroup key="deviceCount">
<FormLabel>{t('devices.flyouts.new.count.label')}</FormLabel>
<FormControl link={this.countLink} type="text" onChange={this.formControlChange} />
</FormGroup>,
<FormGroup key="deviceId">
<FormLabel>{t('devices.flyouts.new.deviceIdExample.label')}</FormLabel>
<div className="device-id-example">{t('devices.flyouts.new.deviceIdExample.format', { deviceName })}</div>
</FormGroup>,
<FormGroup key="deviceModel">
<FormLabel>{t('devices.flyouts.new.deviceModel.label')}</FormLabel>
<FormControl link={this.deviceModelLink} type="select" options={deviceModelOptions} placeholder={t('devices.flyouts.new.deviceModel.hint')} onChange={this.formControlChange} />
</FormGroup>
<FormGroup className="sub-settings">
<FormLabel>{isX509 ? t('devices.flyouts.new.authenticationKey.secondaryThumbprint') : t('devices.flyouts.new.authenticationKey.secondaryKey')}</FormLabel>
<FormControl link={this.secondaryKeyLink} disabled={isGenerateKeys} type="text" placeholder={t('devices.flyouts.new.authenticationKey.hint')} onChange={this.formControlChange} />
</FormGroup>
</FormGroup>
]
}
</div>
<SummarySection>
<SectionHeader>{t('devices.flyouts.new.summaryHeader')}</SectionHeader>
<SummaryBody>
<SummaryCount>{summaryCount || 0}</SummaryCount>
<SectionDesc>{summaryMessage}</SectionDesc>
{this.state.isPending && <Indicator />}
{completedSuccessfully && <Svg className="summary-icon" path={svgs.apply} />}
{completedSuccessfully && isSimulatedDevice &&
t('devices.flyouts.new.simulatedRefreshMessage')
]
}
</SummaryBody>
</SummarySection>
{
!isSimulatedDevice && [
<FormGroup key="deviceCount">
<FormLabel>{t('devices.flyouts.new.count.label')}</FormLabel>
<div className="device-count">{this.countLink.value}</div>
</FormGroup>,
<FormGroup key="deviceId">
<FormLabel>{t('devices.flyouts.new.deviceId.label')}</FormLabel>
<Radio link={this.isGenerateIdLink} value={deviceIdTypeOptions.manual.value} onChange={this.formControlChange}>
<FormControl className="device-id" link={this.deviceIdLink} disabled={isGenerateId} type="text" placeholder={t(deviceIdTypeOptions.manual.hintName)} onChange={this.formControlChange} />
</Radio>
<Radio link={this.isGenerateIdLink} value={deviceIdTypeOptions.generate.value} onChange={this.formControlChange}>
{t(deviceIdTypeOptions.generate.labelName)}
</Radio>
</FormGroup>,
<FormGroup key="authType">
<FormLabel>{t(authTypeOptions.labelName)}</FormLabel>
<Radio link={this.authenticationTypeLink} value={authTypeOptions.symmetric.value} onChange={this.formControlChange}>
{t(authTypeOptions.symmetric.labelName)}
</Radio>
<Radio link={this.authenticationTypeLink} value={authTypeOptions.x509.value} onChange={this.formControlChange}>
{t(authTypeOptions.x509.labelName)}
</Radio>
</FormGroup>,
<FormGroup key="authKeyType">
<FormLabel>{t(authKeyTypeOptions.labelName)}</FormLabel>
<Radio link={this.isGenerateKeysLink} value={authKeyTypeOptions.generate.value} disabled={isX509} onChange={this.formControlChange}>
{t(authKeyTypeOptions.generate.labelName)}
</Radio>
<Radio link={this.isGenerateKeysLink} value={authKeyTypeOptions.manual.value} onChange={this.formControlChange}>
{t(authKeyTypeOptions.manual.labelName)}
</Radio>
<FormGroup className="sub-settings">
<FormLabel>{isX509 ? t('devices.flyouts.new.authenticationKey.primaryThumbprint') : t('devices.flyouts.new.authenticationKey.primaryKey')}</FormLabel>
<FormControl link={this.primaryKeyLink} disabled={isGenerateKeys} type="text" placeholder={t('devices.flyouts.new.authenticationKey.hint')} onChange={this.formControlChange} />
</FormGroup>
<FormGroup className="sub-settings">
<FormLabel>{isX509 ? t('devices.flyouts.new.authenticationKey.secondaryThumbprint') : t('devices.flyouts.new.authenticationKey.secondaryKey')}</FormLabel>
<FormControl link={this.secondaryKeyLink} disabled={isGenerateKeys} type="text" placeholder={t('devices.flyouts.new.authenticationKey.hint')} onChange={this.formControlChange} />
</FormGroup>
</FormGroup>
]
}
</div>
<SummarySection>
<SectionHeader>{t('devices.flyouts.new.summaryHeader')}</SectionHeader>
<SummaryBody>
<SummaryCount>{summaryCount || 0}</SummaryCount>
<SectionDesc>{summaryMessage}</SectionDesc>
{this.state.isPending && <Indicator />}
{completedSuccessfully && <Svg className="summary-icon" path={svgs.apply} />}
{completedSuccessfully && isSimulatedDevice &&
t('devices.flyouts.new.simulatedRefreshMessage')
}
</SummaryBody>
</SummarySection>
{error && <AjaxError className="devices-new-error" t={t} error={error} />}
{
!changesApplied &&
<BtnToolbar>
<Btn primary={true} disabled={isPending || !this.formIsValid()} type="submit">{t('devices.flyouts.new.apply')}</Btn>
<Btn svg={svgs.cancelX} onClick={onClose}>{t('devices.flyouts.new.cancel')}</Btn>
</BtnToolbar>
}
{
!!changesApplied && [
<ProvisionedDevice key="provDevice" device={provisionedDevice} t={t} />,
<BtnToolbar key="buttons">
<Btn svg={svgs.cancelX} onClick={onClose}>{t('devices.flyouts.new.close')}</Btn>
{error && <AjaxError className="devices-new-error" t={t} error={error} />}
{
!changesApplied &&
<BtnToolbar>
<Btn primary={true} disabled={isPending || !this.formIsValid()} type="submit">{t('devices.flyouts.new.apply')}</Btn>
<Btn svg={svgs.cancelX} onClick={onClose}>{t('devices.flyouts.new.cancel')}</Btn>
</BtnToolbar>
]
}
</form>
}
{
!!changesApplied && [
<ProvisionedDevice key="provDevice" device={provisionedDevice} t={t} />,
<BtnToolbar key="buttons">
<Btn svg={svgs.cancelX} onClick={onClose}>{t('devices.flyouts.new.close')}</Btn>
</BtnToolbar>
]
}
</form>
</Protected>
</FlyoutContent>
</Flyout>
);

Просмотреть файл

@ -5,8 +5,17 @@ import { Trans } from 'react-i18next';
import { Observable, Subject } from 'rxjs';
import Config from 'app.config';
import { permissions } from 'services/models';
import { RulesGrid } from 'components/pages/rules/rulesGrid';
import { AjaxError, Btn, PageContent, ContextMenu, RefreshBar, Indicator } from 'components/shared';
import {
AjaxError,
Btn,
ContextMenu,
Indicator,
PageContent,
Protected,
RefreshBar
} from 'components/shared';
import { svgs, joinClasses, renderUndefined } from 'utilities';
import { DevicesGrid } from 'components/pages/devices/devicesGrid';
import { DeviceGroupDropdownContainer as DeviceGroupDropdown } from 'components/app/deviceGroupDropdown';
@ -194,15 +203,21 @@ export class RuleDetails extends Component {
const alertContextBtns =
selectedRows.length > 0
? [
<Btn svg={svgs.closeAlert} onClick={this.closeAlerts} key="close">
<Trans i18nKey="maintenance.close">Close</Trans>
</Btn>,
<Btn svg={svgs.ackAlert} onClick={this.ackAlerts} key="ack">
<Trans i18nKey="maintenance.acknowledge">Acknowledge</Trans>
</Btn>,
<Btn svg={svgs.trash} onClick={this.deleteAlerts} key="delete">
<Trans i18nKey="maintenance.delete">Delete</Trans>
</Btn>
<Protected permission={permissions.updateAlarms}>
<Btn svg={svgs.closeAlert} onClick={this.closeAlerts} key="close">
<Trans i18nKey="maintenance.close">Close</Trans>
</Btn>
</Protected>,
<Protected permission={permissions.updateAlarms}>
<Btn svg={svgs.ackAlert} onClick={this.ackAlerts} key="ack">
<Trans i18nKey="maintenance.acknowledge">Acknowledge</Trans>
</Btn>
</Protected>,
<Protected permission={permissions.deleteAlarms}>
<Btn svg={svgs.trash} onClick={this.deleteAlerts} key="delete">
<Trans i18nKey="maintenance.delete">Delete</Trans>
</Btn>
</Protected>
]
: null;
this.setState({

Просмотреть файл

@ -1,6 +1,8 @@
// Copyright (c) Microsoft. All rights reserved.
import React from 'react';
import { permissions } from 'services/models';
import { Protected, ProtectedError } from 'components/shared';
import { RuleEditorContainer } from './ruleEditor';
import Flyout from 'components/shared/flyout';
@ -11,7 +13,17 @@ export const EditRuleFlyout = ({ t, onClose, rule }) => (
<Flyout.CloseBtn onClick={onClose} />
</Flyout.Header>
<Flyout.Content>
<RuleEditorContainer onClose={onClose} rule={rule} />
<Protected permission={permissions.updateRules}>{
(hasPermission, permission) =>
hasPermission
? <RuleEditorContainer onClose={onClose} rule={rule} />
:
<div>
<ProtectedError t={t} permission={permission} />
<p>A read-only view will be added soon as part of another PBI.</p>
</div>
}
</Protected>
</Flyout.Content>
</Flyout.Container>
);

Просмотреть файл

@ -1,6 +1,8 @@
// Copyright (c) Microsoft. All rights reserved.
import React from 'react';
import { permissions } from 'services/models';
import { Protected } from 'components/shared';
import { RuleEditorContainer } from './ruleEditor';
import Flyout from 'components/shared/flyout';
@ -11,7 +13,9 @@ export const NewRuleFlyout = ({ t, onClose }) => (
<Flyout.CloseBtn onClick={onClose} />
</Flyout.Header>
<Flyout.Content>
<RuleEditorContainer onClose={onClose} />
<Protected permission={permissions.createRules}>
<RuleEditorContainer onClose={onClose} />
</Protected>
</Flyout.Content>
</Flyout.Container>
);

Просмотреть файл

@ -4,10 +4,12 @@ import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import { Trans } from 'react-i18next';
import { permissions } from 'services/models';
import {
AjaxError,
Btn,
BtnToolbar,
Protected,
Svg
} from 'components/shared';
import { svgs } from 'utilities';
@ -112,15 +114,17 @@ export class DeleteRule extends Component {
<Flyout.CloseBtn onClick={onClose} />
</Flyout.Header>
<Flyout.Content>
<form onSubmit={this.deleteRule} className="delete-rule-flyout-container">
{rule && <RuleSummary rule={rule} isPending={isPending} completedSuccessfully={completedSuccessfully} t={t} className="rule-details"/>}
{error && <AjaxError className="rule-delete-error" t={t} error={error} />}
{!error &&
(changesApplied
? this.renderConfirmation()
: this.renderButtons())
}
</form>
<Protected permission={permissions.deleteRules}>
<form onSubmit={this.deleteRule} className="delete-rule-flyout-container">
{rule && <RuleSummary rule={rule} isPending={isPending} completedSuccessfully={completedSuccessfully} t={t} className="rule-details"/>}
{error && <AjaxError className="rule-delete-error" t={t} error={error} />}
{!error &&
(changesApplied
? this.renderConfirmation()
: this.renderButtons())
}
</form>
</Protected>
</Flyout.Content>
</Flyout.Container>
);

Просмотреть файл

@ -7,11 +7,12 @@ import {
AjaxError,
Btn,
BtnToolbar,
Protected,
ToggleBtn
} from 'components/shared';
import { svgs } from 'utilities';
import { TelemetryService } from 'services';
import { toNewRuleRequestModel } from 'services/models';
import { permissions, toNewRuleRequestModel } from 'services/models';
import Flyout from 'components/shared/flyout';
import './ruleStatus.css';
@ -84,28 +85,30 @@ export class RuleStatus extends Component {
<Flyout.CloseBtn onClick={onClose} />
</Flyout.Header>
<Flyout.Content>
<form onSubmit={this.changeRuleStatus} className="disable-rule-flyout-container">
<div className="padded-top-bottom">
<ToggleBtn
value={status}
onChange={this.onToggle} >
{status ? t('rules.flyouts.enable') : t('rules.flyouts.disable')}
</ToggleBtn>
</div>
{
rules.map((rule) => (
<RuleSummary rule={rule} isPending={isPending} completedSuccessfully={completedSuccessfully} t={t} />
))
}
<Protected permission={permissions.updateRules}>
<form onSubmit={this.changeRuleStatus} className="disable-rule-flyout-container">
<div className="padded-top-bottom">
<ToggleBtn
value={status}
onChange={this.onToggle} >
{status ? t('rules.flyouts.enable') : t('rules.flyouts.disable')}
</ToggleBtn>
</div>
{
rules.map((rule) => (
<RuleSummary rule={rule} isPending={isPending} completedSuccessfully={completedSuccessfully} t={t} />
))
}
{error && <AjaxError className="rule-status-error" t={t} error={error} />}
{
<BtnToolbar>
<Btn primary={true} disabled={!!changesApplied || isPending} type="submit">{t('rules.flyouts.ruleEditor.apply')}</Btn>
<Btn svg={svgs.cancelX} onClick={onClose}>{t('rules.flyouts.ruleEditor.cancel')}</Btn>
</BtnToolbar>
}
</form>
{error && <AjaxError className="rule-status-error" t={t} error={error} />}
{
<BtnToolbar>
<Btn primary={true} disabled={!!changesApplied || isPending} type="submit">{t('rules.flyouts.ruleEditor.apply')}</Btn>
<Btn svg={svgs.cancelX} onClick={onClose}>{t('rules.flyouts.ruleEditor.cancel')}</Btn>
</BtnToolbar>
}
</form>
</Protected>
</Flyout.Content>
</Flyout.Container>
);

Просмотреть файл

@ -1,10 +1,19 @@
// Copyright (c) Microsoft. All rights reserved.
import React, { Component } from 'react';
import { permissions } from 'services/models';
import { RulesGrid } from './rulesGrid';
import { DeviceGroupDropdownContainer as DeviceGroupDropdown } from 'components/app/deviceGroupDropdown';
import { ManageDeviceGroupsBtnContainer as ManageDeviceGroupsBtn } from 'components/app/manageDeviceGroupsBtn';
import { AjaxError, Btn, RefreshBar, PageContent, ContextMenu, SearchInput } from 'components/shared';
import {
AjaxError,
Btn,
ContextMenu,
PageContent,
Protected,
RefreshBar,
SearchInput
} from 'components/shared';
import { NewRuleFlyout } from './flyouts';
import { svgs } from 'utilities';
@ -77,7 +86,9 @@ export class Rules extends Component {
<DeviceGroupDropdown />
<SearchInput onChange={this.searchOnChange} placeholder={t('rules.searchPlaceholder')} />
{this.state.contextBtns}
<Btn svg={svgs.plus} onClick={this.openNewRuleFlyout}>New rule</Btn>
<Protected permission={permissions.createRules}>
<Btn svg={svgs.plus} onClick={this.openNewRuleFlyout}>{t('rules.flyouts.newRule')}</Btn>
</Protected>
<ManageDeviceGroupsBtn />
</ContextMenu>,
<PageContent className="rules-container" key="page-content">

Просмотреть файл

@ -3,7 +3,8 @@
import React, { Component } from 'react';
import { Trans } from 'react-i18next';
import { Btn, PcsGrid } from 'components/shared';
import { permissions } from 'services/models';
import { Btn, PcsGrid, Protected } from 'components/shared';
import { rulesColumnDefs, defaultRulesGridProps } from './rulesGridConfig';
import { checkboxColumn } from 'components/shared/pcsGrid/pcsGridConfig';
import { isFunc, translateColumnDefs, svgs } from 'utilities';
@ -41,25 +42,35 @@ export class RulesGrid extends Component {
this.contextBtns = {
disable:
<Btn key="disable" className="rule-status-btn" svg={svgs.disableToggle} onClick={this.openStatusFlyout}>
<Trans i18nKey="rules.flyouts.disable">Disable</Trans>
</Btn>,
<Protected permission={permissions.updateRules}>
<Btn key="disable" className="rule-status-btn" svg={svgs.disableToggle} onClick={this.openStatusFlyout}>
<Trans i18nKey="rules.flyouts.disable">Disable</Trans>
</Btn>
</Protected>,
enable:
<Btn key="enable" className="rule-status-btn enabled" svg={svgs.enableToggle} onClick={this.openStatusFlyout}>
<Trans i18nKey="rules.flyouts.enable">Enable</Trans>
</Btn>,
<Protected permission={permissions.updateRules}>
<Btn key="enable" className="rule-status-btn enabled" svg={svgs.enableToggle} onClick={this.openStatusFlyout}>
<Trans i18nKey="rules.flyouts.enable">Enable</Trans>
</Btn>
</Protected>,
changeStatus:
<Btn key="changeStatus" className="rule-status-btn" svg={svgs.changeStatus} onClick={this.openStatusFlyout}>
<Trans i18nKey="rules.flyouts.changeStatus">Change status</Trans>
</Btn>,
<Protected permission={permissions.updateRules}>
<Btn key="changeStatus" className="rule-status-btn" svg={svgs.changeStatus} onClick={this.openStatusFlyout}>
<Trans i18nKey="rules.flyouts.changeStatus">Change status</Trans>
</Btn>
</Protected>,
edit:
<Btn key="edit" svg={svgs.edit} onClick={this.openEditRuleFlyout}>
{props.t('rules.flyouts.edit')}
</Btn>,
<Protected permission={permissions.updateRules}>
<Btn key="edit" svg={svgs.edit} onClick={this.openEditRuleFlyout}>
{props.t('rules.flyouts.edit')}
</Btn>
</Protected>,
delete:
<Btn key="delete" svg={svgs.trash} onClick={this.openDeleteFlyout}>
<Trans i18nKey="rules.flyouts.delete">Delete</Trans>
</Btn>
<Protected permission={permissions.deleteRules}>
<Btn key="delete" svg={svgs.trash} onClick={this.openDeleteFlyout}>
<Trans i18nKey="rules.flyouts.delete">Delete</Trans>
</Btn>
</Protected>
};
}

Просмотреть файл

@ -9,5 +9,6 @@ export * from './forms';
export * from './indicator/indicator';
export * from './pageContent/pageContent';
export * from './pcsGrid/pcsGrid';
export * from './protected'
export * from './refreshBar/refreshBar';
export * from './svg/svg';

Просмотреть файл

@ -0,0 +1,46 @@
Protected Components
=================================
These components are intended to enforce user permissions in the UI layer.
The individual permissions are defined with the [models for the auth microservice](../../../services/models/authModels.js).
## Using the Protected component
React components allow you to pass functions as children instead of pure JSX. So the Protected component can have two use cases
enabling it to be simple, declarative, highly customizable, and hide the permission logic from the rest of the app.
### Use case 1: 
Pass pure JSX as the children will either render or not render the children based on the permission status.
<Protected permissions={permissions.deleteDevices}>
  <Btn>Protected Button</Btn>
</Protected>
 
### Use case 2: 
Pass a function as the children. In this case, the Protected component will pass a boolean indicating if the user has the required permission or not.
<Protected permissions={permissions.deleteDevices}>
  {
    (hasPermission) => hasPermission 
      ? <FormControl link={this.userEmails}> 
      : <span>You don't have permission to edit user emails :(</span>
  }
</Protected>
Additionally, a parameter containing the permission will also be passed. See below for an example of its use with the ProtectedError component.
## Using the optional ProtectedError component
If you'd like to show a default message instead of simply hiding the children, use case 2 above along with the ProtectedError component can assist.
<Protected permissions={permissions.deleteDevices}>
  {
    (hasPermission, permission) => hasPermission 
      ? <FormControl link={this.userEmails}> 
      : <ProtectedError t={t} permission={permission} /> 
  }
</Protected>

Просмотреть файл

@ -0,0 +1,4 @@
// Copyright (c) Microsoft. All rights reserved.
export * from './protected.container';
export * from './protectedError';

Просмотреть файл

@ -0,0 +1,11 @@
// Copyright (c) Microsoft. All rights reserved.
import { connect } from 'react-redux';
import { getUserPermissions } from 'store/reducers/appReducer';
import { ProtectedImpl } from './protected.impl';
const mapStateToProps = state => ({
userPermissions: getUserPermissions(state)
});
export const Protected = connect(mapStateToProps, null)(ProtectedImpl);

Просмотреть файл

@ -0,0 +1,20 @@
// Copyright (c) Microsoft. All rights reserved.
import { Component } from 'react';
import { isFunc } from 'utilities';
export class ProtectedImpl extends Component {
userHasPermission() {
const { permission, userPermissions } = this.props;
return userPermissions.has(permission);
}
render() {
const { children, permission } = this.props;
const hasPermission = this.userHasPermission();
if (isFunc(children)) {
return children(hasPermission, permission);
}
return hasPermission ? children : null;
}
};

Просмотреть файл

@ -0,0 +1,10 @@
// Copyright (c) Microsoft. All rights reserved.
import React from 'react';
import { ErrorMsg } from '../forms';
import { joinClasses } from 'utilities';
export const ProtectedError = ({ permission, t, className }) => (
<ErrorMsg className={joinClasses('protected-error', className)}>{t('protected.permissionDenied', { permission })}</ErrorMsg>
);

Просмотреть файл

@ -0,0 +1,22 @@
// Copyright (c) Microsoft. All rights reserved.
export const permissions = {
createDeviceGroups: 'CreateDeviceGroups',
deleteDeviceGroups: 'DeleteDeviceGroups',
updateDeviceGroups: 'UpdateDeviceGroups',
createDevices: 'CreateDevices',
deleteDevices: 'DeleteDevices',
updateDevices: 'UpdateDevices',
createRules: 'CreateRules',
deleteRules: 'DeleteRules',
updateRules: 'UpdateRules',
deleteAlarms: 'DeleteAlarms',
updateAlarms: 'UpdateAlarms',
createJobs: 'CreateJobs',
updateSIMManagement: 'UpdateSIMManagement'
};

Просмотреть файл

@ -3,6 +3,7 @@
// Exports models
export * from './ajaxModels';
export * from './authModels';
export * from './deviceSimulationModels';
export * from './iotHubManagerModels';
export * from './telemetryModels';

Просмотреть файл

@ -6,6 +6,7 @@ import { ConfigService, GitHubService } from 'services';
import { schema, normalize } from 'normalizr';
import { createSelector } from 'reselect';
import update from 'immutability-helper';
import { permissions } from 'services/models';
import {
createAction,
createReducerScenario,
@ -121,7 +122,29 @@ const initialState = {
isDefaultLogo: true,
azureMapsKey: '',
deviceGroupFlyoutIsOpen: false,
timeInterval: 'PT1H'
timeInterval: 'PT1H',
//TODO: Get this from the server. This is just hardcoded test data for now.
userPermissions: new Set([
permissions.createDeviceGroups,
permissions.deleteDeviceGroups,
permissions.updateDeviceGroups,
permissions.createDevices,
permissions.deleteDevices,
permissions.updateDevices,
permissions.createRules,
permissions.deleteRules,
permissions.updateRules,
permissions.deleteAlarms,
permissions.updateAlarms,
permissions.createJobs,
permissions.updateSIMManagement
])
};
const updateDeviceGroupsReducer = (state, { payload, fromAction }) => {
@ -247,4 +270,6 @@ export const getLogoPendingStatus = state =>
getPending(getAppReducer(state), epics.actionTypes.fetchLogo);
export const getTimeInterval = state => getAppReducer(state).timeInterval;
export const getUserPermissions = state => getAppReducer(state).userPermissions;
// ========================= Selectors - END