Merge pull request #2110 from a1mery/app-secret-expiration-groupViews

[react-graph-secret-expiration] - Add group view options
This commit is contained in:
Hugo Bernier 2021-11-05 16:37:50 -04:00 committed by GitHub
commit 1c77ae3b21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 285 additions and 41 deletions

2
.gitignore vendored
View File

@ -35,3 +35,5 @@ obj
packages/
samples/react-teams-send-notification/MyNotes.md
samples/react-teams-send-notification/teams/teams.zip
samples/react-find-parker/release/component-dependency-audit/react-find-parker.json
samples/react-find-parker/release/webpack-stats/react-find-parker.stats.json

View File

@ -33,6 +33,7 @@ Version|Date|Comments
-------|----|--------
1.0|September 17, 2021|Initial release
1.1|October 10, 2021|Add pagination
1.2|November 04, 2021|Add group views
## Minimal Path to Awesome

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 MiB

After

Width:  |  Height:  |  Size: 18 MiB

View File

@ -9,7 +9,7 @@
"This sample web part shows the list of your applications registered in Azure AD along with their associated client secret/certificate expiration date."
],
"creationDateTime": "2021-09-17",
"updateDateTime": "2021-10-10",
"updateDateTime": "2021-11-04",
"products": [
"SharePoint",
"Office"

View File

@ -3,7 +3,7 @@
"solution": {
"name": "react-graph-app-secret-expiration-client-side-solution",
"id": "b25d85a4-7310-408a-a263-25959b0a5b1b",
"version": "1.1.0.0",
"version": "1.2.0.0",
"includeClientSideAssets": true,
"isDomainIsolated": false,
"skipFeatureDeployment": true,

View File

@ -1,6 +1,6 @@
{
"name": "react-graph-app-secret-expiration",
"version": "1.1.0",
"version": "1.2.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@ -1,6 +1,6 @@
{
"name": "react-graph-app-secret-expiration",
"version": "1.1.0",
"version": "1.2.0",
"private": true,
"main": "lib/index.js",
"scripts": {

View File

@ -12,7 +12,7 @@ export interface IApplication {
export interface IPasswordCredential {
customKeyIdentifier: string;
displayName: string;
endDateTime: Date;
endDateTime: string;
hint: string;
keyId: string;
secretText: string;
@ -22,7 +22,7 @@ export interface IPasswordCredential {
export interface IKeyCredential {
customKeyIdentifier: string;
displayName: string;
endDateTime: Date;
endDateTime: string;
key: string;
keyId: string;
startDateTime: string;
@ -37,4 +37,5 @@ export interface IFormattedApplication {
daysLeft: number;
type: string;
expirationDate: string;
linkTitle: string;
}

View File

@ -0,0 +1,175 @@
{
"applications": [
{
"appId": "bb4bcecc-d0cb-478a-9d48-1527cb7b65c0",
"displayName": "API Backend",
"keyCredentials": [],
"passwordCredentials": [
{
"customKeyIdentifier": null,
"displayName": "Password friendly name",
"endDateTime": "2020-09-20T15:47:46.5876682Z",
"hint": "42",
"keyId": "c2435363-4f50-4c14-8994-0122e77d84c9",
"secretText": null,
"startDateTime": "2021-09-20T15:47:46.5876682Z"
},
{
"customKeyIdentifier": null,
"displayName": "Postman",
"endDateTime": "2021-11-25T16:25:17.375Z",
"hint": "42",
"keyId": "7ead14df-56e8-4a24-9f6e-6692ae17e84f",
"secretText": null,
"startDateTime": "2021-09-20T15:25:17.375Z"
},
{
"customKeyIdentifier": null,
"displayName": "PnP",
"endDateTime": "2022-02-04T06:58:04.525Z",
"hint": "42",
"keyId": "8fc28fb8-ab39-4623-bd75-6d7baf5f205d",
"secretText": null,
"startDateTime": "2021-02-04T06:58:17.678Z"
},
{
"customKeyIdentifier": null,
"displayName": "Dev",
"endDateTime": "2021-12-15T16:36:58.443Z",
"hint": "42",
"keyId": "ef77eda1-741c-4b51-9de7-aa455831dff0",
"secretText": null,
"startDateTime": "2020-12-15T16:37:05.242Z"
}
]
},
{
"appId": "8537b74d-1bfe-41ba-849a-eb25917d3746",
"displayName": "Client API",
"keyCredentials": [],
"passwordCredentials": [
{
"customKeyIdentifier": null,
"displayName": "Password friendly name",
"endDateTime": "2022-09-20T15:47:46.5876682Z",
"hint": "42",
"keyId": "7e6dd1c3-2366-4c5a-9a3f-d1c83f40ba11",
"secretText": null,
"startDateTime": "2021-09-20T15:47:46.5876682Z"
},
{
"customKeyIdentifier": null,
"displayName": "Password friendly name",
"endDateTime": "2021-12-05T15:47:46.5876682Z",
"hint": "42",
"keyId": "7c341ab9-089e-47e1-946b-60b155c3e94c",
"secretText": null,
"startDateTime": "2021-09-20T15:47:46.5876682Z"
},
{
"customKeyIdentifier": null,
"displayName": "Password friendly name",
"endDateTime": "2021-12-01T15:47:46.5876682Z",
"hint": "42",
"keyId": "24d724d8-71b3-471e-9b92-a5bd7e9bb6d6",
"secretText": null,
"startDateTime": "2021-09-20T15:47:46.5876682Z"
},
{
"customKeyIdentifier": null,
"displayName": "Password friendly name",
"endDateTime": "2021-11-20T15:47:46.5876682Z",
"hint": "42",
"keyId": "4488b5fa-01a0-4191-a96e-de3a83cae203",
"secretText": null,
"startDateTime": "2021-11-20T15:47:46.5876682Z"
}
]
},
{
"appId": "e2bfaff8-9a03-4837-886e-b110b4972a47",
"displayName": "Graph Console App",
"keyCredentials": [],
"passwordCredentials": [
{
"customKeyIdentifier": null,
"displayName": "Password friendly name",
"endDateTime": "2021-12-28T15:47:46.5876682Z",
"hint": "42",
"keyId": "7791e9be-defd-4588-9612-ea6404af1875",
"secretText": null,
"startDateTime": "2021-09-20T15:47:46.5876682Z"
},
{
"customKeyIdentifier": null,
"displayName": "Password friendly name",
"endDateTime": "2022-01-03T15:47:46.5876682Z",
"hint": "42",
"keyId": "8962ac05-6a3e-4f9a-b91f-e0212cd9847b",
"secretText": null,
"startDateTime": "2021-09-20T15:47:46.5876682Z"
},
{
"customKeyIdentifier": null,
"displayName": "Password friendly name",
"endDateTime": "2022-11-18T15:47:46.5876682Z",
"hint": "42",
"keyId": "85bc5551-1165-4e04-aa77-0d0c8f32ab1f",
"secretText": null,
"startDateTime": "2021-09-20T15:47:46.5876682Z"
},
{
"customKeyIdentifier": null,
"displayName": "Password friendly name",
"endDateTime": "2020-12-28T15:47:46.5876682Z",
"hint": "42",
"keyId": "cd909eca-bc21-4005-b346-7e2ef47fe26b",
"secretText": null,
"startDateTime": "2021-09-20T15:47:46.5876682Z"
}
]
},
{
"appId": "b166b3ca-38e0-4dd5-80e4-03f51509a8eb",
"displayName": "PnP Modernization Scanner",
"keyCredentials": [
{
"customKeyIdentifier": "SampleId",
"displayName": "PnP Modernization Scanner",
"endDateTime": "2031-01-11T23:00:00Z",
"key": "SampleKey",
"keyId": "9942dd18-a265-422f-bbfe-b8fdd866c3d3",
"startDateTime": "2021-01-11T23:00:00Z",
"type": "AsymmetricX509Cert",
"usage": "Verify"
}
],
"passwordCredentials": []
},
{
"appId": "14ce9f15-41ed-446b-8733-a0eeb1379b38",
"displayName": "Bot - OfficeDev",
"keyCredentials": [],
"passwordCredentials": [
{
"customKeyIdentifier": null,
"displayName": "Password friendly name",
"endDateTime": "2023-09-20T15:47:46.5876682Z",
"hint": "42",
"keyId": "cb8cf8ec-630b-4da6-a578-4d27742d069c",
"secretText": null,
"startDateTime": "2021-09-20T15:47:46.5876682Z"
},
{
"customKeyIdentifier": null,
"displayName": "Password friendly name",
"endDateTime": "2024-09-20T15:47:46.5876682Z",
"hint": "42",
"keyId": "2bfdc738-db27-4b3d-898d-ce3f57ab81eb",
"secretText": null,
"startDateTime": "2021-09-20T15:47:46.5876682Z"
}
]
}
]
}

View File

@ -3,7 +3,9 @@ import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
IPropertyPaneConfiguration,
PropertyPaneTextField
IPropertyPaneDropdownOption,
PropertyPaneDropdown,
PropertyPaneToggle
} from '@microsoft/sp-property-pane';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
@ -13,11 +15,18 @@ import { IGraphAppSecretExpirationProps } from './components/IGraphAppSecretExpi
import { MSGraphClient } from '@microsoft/sp-http';
export interface IGraphAppSecretExpirationWebPartProps {
description: string;
groupByColumn: string;
expiringSoon: boolean;
displaySampleData: boolean;
}
export default class GraphAppSecretExpirationWebPart extends BaseClientSideWebPart<IGraphAppSecretExpirationWebPartProps> {
private graphClient: MSGraphClient;
private dropdownOptions: IPropertyPaneDropdownOption[] = [
{ key: "none", text: "None" },
{ key: "applicationId", text: "Application ID" },
{ key: "type", text: "Type" }];
public onInit(): Promise<void> {
return new Promise<void>((resolve: () => void, reject: (error: any) => void): void => {
@ -34,8 +43,10 @@ export default class GraphAppSecretExpirationWebPart extends BaseClientSideWebPa
const element: React.ReactElement<IGraphAppSecretExpirationProps> = React.createElement(
GraphAppSecretExpiration,
{
description: this.properties.description,
graphClient: this.graphClient
graphClient: this.graphClient,
groupByColumn: this.properties.groupByColumn,
expiringSoon: this.properties.expiringSoon,
displaySampleData: this.properties.displaySampleData
}
);
@ -61,8 +72,27 @@ export default class GraphAppSecretExpirationWebPart extends BaseClientSideWebPa
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField('description', {
label: strings.DescriptionFieldLabel
PropertyPaneDropdown('groupByColumn', {
label: strings.DefaultGroupColumnFieldLabel,
options: this.dropdownOptions,
selectedKey: "none"
}),
PropertyPaneToggle('expiringSoon', {
label: strings.DisplayOnlySecretsFieldLabel,
onText: "Yes",
offText: "No"
})
]
},
{
groupName: strings.OtherGroupName,
groupFields: [
PropertyPaneToggle('displaySampleData', {
label: strings.DisplaySampleDataFieldLabel,
onText: "Yes",
offText: "No"
})
]
}

View File

@ -4,7 +4,8 @@
.container {
max-width: 900px;
margin: 0px auto;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
border: 1px solid lightgray
//box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.row {

View File

@ -5,9 +5,10 @@ import { ListView, IViewField, SelectionMode, GroupOrder, IGrouping } from "@pnp
import { IApplications, IApplication, IFormattedApplication } from '../../../models/IApplication';
import { IGraphAppSecretExpirationState } from './GraphAppSecretExpirationState';
import * as moment from 'moment';
import { DefaultButton, Spinner, mergeStyles, SearchBox } from '@fluentui/react';
import { Spinner, mergeStyles, SearchBox } from '@fluentui/react';
import { Pagination } from "@pnp/spfx-controls-react/lib/pagination";
import * as strings from 'GraphAppSecretExpirationWebPartStrings';
import sampleApplications from '../../../models/SampleApplications.json';
const stackItemHidden = mergeStyles({
display: 'none',
@ -16,7 +17,8 @@ const _viewFields: IViewField[] = [
{
name: "applicationId",
displayName: "Application ID",
minWidth: 250
minWidth: 250,
linkPropertyName: "linkTitle"
},
{
name: "displayName",
@ -71,7 +73,12 @@ export default class GraphAppSecretExpiration extends React.Component<IGraphAppS
public componentDidMount(): void {
this._getApplications();
if (!this.props.displaySampleData) {
this._getApplications();
} else {
this._propertiesMapping(sampleApplications.applications);
}
}
private _getApplications(): IApplication[] {
@ -126,9 +133,16 @@ export default class GraphAppSecretExpiration extends React.Component<IGraphAppS
displayName: app.displayName,
type: "Secret",
expirationDate: (moment(pswd.endDateTime)).format('DD-MMM-YYYY'),
daysLeft: daysBeforeExpiration > 0 ? daysBeforeExpiration : 0
daysLeft: daysBeforeExpiration > 0 ? daysBeforeExpiration : 0,
linkTitle: "https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/Credentials/appId/" + app.appId
};
displayedApplication.push(formatedApp);
if (this.props.expiringSoon) {
if (daysBeforeExpiration < 30) {
displayedApplication.push(formatedApp);
}
} else {
displayedApplication.push(formatedApp);
}
});
app.keyCredentials.forEach(keyCred => {
let daysBeforeExpiration = moment.duration((moment(keyCred.endDateTime)).diff(today, 'days'), 'days').asDays();
@ -137,18 +151,27 @@ export default class GraphAppSecretExpiration extends React.Component<IGraphAppS
displayName: app.displayName,
type: "Certificate",
expirationDate: (moment(keyCred.endDateTime)).format('DD-MMM-YYYY'),
daysLeft: daysBeforeExpiration > 0 ? daysBeforeExpiration : 0
daysLeft: daysBeforeExpiration > 0 ? daysBeforeExpiration : 0,
linkTitle: "https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/Credentials/appId/" + app.appId
};
displayedApplication.push(formatedApp);
if (this.props.expiringSoon) {
if (daysBeforeExpiration < 30) {
displayedApplication.push(formatedApp);
}
} else {
displayedApplication.push(formatedApp);
}
});
});
this.setState({
applications: displayedApplication,
filteredApplications: displayedApplication
filteredApplications: displayedApplication,
loading: false
});
} catch (error) {
console.log(error);
}
this._groupView();
}
private _getPage(selectedPage: number) {
@ -194,10 +217,10 @@ export default class GraphAppSecretExpiration extends React.Component<IGraphAppS
}
private _groupView = () => {
if (this.state.groupByFields.length === 0) {
if (this.props.groupByColumn !== "none") {
let groupByFields: IGrouping[] = [
{
name: "applicationId",
name: this.props.groupByColumn,
order: GroupOrder.ascending
}
];
@ -218,14 +241,13 @@ export default class GraphAppSecretExpiration extends React.Component<IGraphAppS
<div className={styles.graphAppSecretExpiration}>
<div className={styles.container}>
<br />
<p className={styles.title}>Application list :</p>
<p className={styles.title}>Certificates and secrets :</p>
<div className={this.state.loading ? "" : stackItemHidden}>
<Spinner label="Loading..." ariaLive="assertive" labelPosition="left" />
<br />
</div>
<div className={this.state.loading ? stackItemHidden : ""}>
<DefaultButton text={this.state.groupByFields.length === 0 ? "Group by App ID" : "Ungroup"} onClick={this._groupView} />
<SearchBox placeholder="Search" onChange={(e, text) => this._filterApplication(text, false)} onClear={() => this._filterApplication("", true)} value={this.state.filterValue} />
<SearchBox placeholder="Search" onChange={(e, text) => this._filterApplication(text, true)} onClear={() => this._filterApplication("", false)} value={this.state.filterValue} />
<ListView
items={this.state.filteredApplications.slice(this.state.page === 1 || this.state.filterValue !== "" ? 0 : this.state.page * 10 - 10, this.state.page * 10)}
viewFields={_viewFields}

View File

@ -1,13 +1,13 @@
import { IGrouping } from '@pnp/spfx-controls-react/lib/ListView';
import { IApplications, IApplication,IPasswordCredential,IKeyCredential,IFormattedApplication } from '../../../models/IApplication';
import { IFormattedApplication } from '../../../models/IApplication';
export interface IGraphAppSecretExpirationState {
applications: IFormattedApplication[];
filteredApplications: IFormattedApplication[];
filterValue: string;
searchFilter: string;
groupByFields: IGrouping[];
page: number;
error: string;
loading: boolean;
}
applications: IFormattedApplication[];
filteredApplications: IFormattedApplication[];
filterValue: string;
searchFilter: string;
groupByFields: IGrouping[];
page: number;
error: string;
loading: boolean;
}

View File

@ -1,6 +1,8 @@
import { MSGraphClient } from "@microsoft/sp-http";
export interface IGraphAppSecretExpirationProps {
description: string;
graphClient: MSGraphClient;
groupByColumn: string;
expiringSoon: boolean;
displaySampleData: boolean;
}

View File

@ -1,7 +1,11 @@
define([], function() {
define([], function () {
return {
"PropertyPaneDescription": "Description",
"BasicGroupName": "Group Name",
"DescriptionFieldLabel": "Description Field"
"PropertyPaneDescription": "Displays list of all Azure AD applications secrets and certificates expiration date.",
"BasicGroupName": "Display settings:",
"OtherGroupName": "Other settings:",
"DescriptionFieldLabel": "Description Field",
"DisplayOnlySecretsFieldLabel": "Display only secrets/certificates that will expire soon:",
"DisplaySampleDataFieldLabel": "Display sample data:",
"DefaultGroupColumnFieldLabel": "Default group column:"
}
});

View File

@ -1,7 +1,11 @@
declare interface IGraphAppSecretExpirationWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
OtherGroupName: string;
DescriptionFieldLabel: string;
DisplayOnlySecretsFieldLabel: string;
DisplaySampleDataFieldLabel: string;
DefaultGroupColumnFieldLabel: string;
}
declare module 'GraphAppSecretExpirationWebPartStrings' {

View File

@ -14,6 +14,8 @@
"inlineSources": false,
"strictNullChecks": false,
"noUnusedLocals": false,
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"typeRoots": [
"./node_modules/@types",
"./node_modules/@microsoft"