Skip to content

Commit 0caa82e

Browse files
authored
Merge pull request #78 from rapid7/issue-65/make-profile-name-configurable
Fix #49 - Implement a "recent logins" list on the Configure page
2 parents 34e54aa + 2c7dd35 commit 0caa82e

File tree

8 files changed

+117
-37
lines changed

8 files changed

+117
-37
lines changed

app.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ Application.on('ready', () => {
7171
mainWindow.loadURL(Server.get('configureUrl'));
7272
mainWindow.show();
7373

74+
// TODO: A global clipboard instance must be loaded. Investigate how to load it within the .jsx code.
75+
mainWindow.webContents.executeJavaScript('new Clipboard(".copy-to-clipboard-button");');
76+
7477
setInterval(() => {
7578
const entryPointUrl = Server.get('entryPointUrl');
7679

lib/routes/configure.js

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,33 @@ const ResponseObj = require('./../response');
1818

1919
module.exports = (app, auth) => {
2020
router.get('/', (req, res) => {
21+
const storedMetadataUrls = Storage.get('metadataUrls') || {};
22+
23+
// We populate the value of the metadata url field on the following (in order of precedence):
24+
// 1. Use the current session's metadata url (may have been rejected).
25+
// 2. Use the latest validated metadata url.
26+
// 3. Support the <= v1.3.0 storage key.
27+
// 4. Default the metadata url to empty string.
28+
const defaultMetadataUrl =
29+
app.get('metadataUrl') ||
30+
Storage.get('previousMetadataUrl') ||
31+
Storage.get('metadataUrl') ||
32+
Object.keys(storedMetadataUrls)[0] ||
33+
'';
34+
2135
res.render('configure', Object.assign(ResponseObj, {
22-
metadataUrl: Storage.get('metadataUrl') || '',
36+
defaultMetadataUrl,
37+
metadataUrls: storedMetadataUrls,
2338
metadataUrlValid: Storage.get('metadataUrlValid'),
2439
error: Storage.get('metadataUrlError')
2540
}));
2641
});
2742

2843
router.post('/', (req, res) => {
2944
const metadataUrl = req.body.metadataUrl;
30-
const metaDataResponseObj = Object.assign(ResponseObj, {metadataUrl});
45+
const metaDataResponseObj = Object.assign(ResponseObj, {defaultMetadataUrl: metadataUrl});
3146

32-
Storage.set('metadataUrl', metadataUrl);
47+
app.set('metadataUrl', metadataUrl);
3348

3449
const xmlReq = https.get(metadataUrl, (xmlRes) => {
3550
let xml = '';
@@ -82,6 +97,15 @@ module.exports = (app, auth) => {
8297
config.auth.entryPoint = entryPoint;
8398

8499
if (cert && issuer && entryPoint) {
100+
Storage.set('previousMetadataUrl', metadataUrl);
101+
let metadataUrls = Storage.get('metadataUrls') || {};
102+
103+
if (!metadataUrls.hasOwnProperty(metadataUrl)) {
104+
metadataUrls[metadataUrl] = metadataUrl
105+
106+
Storage.set('metadataUrls', metadataUrls);
107+
}
108+
85109
app.set('entryPointUrl', config.auth.entryPoint);
86110
auth.configure(config.auth);
87111
res.redirect(config.auth.entryPoint);

lib/routes/refresh.js

Lines changed: 41 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -11,41 +11,52 @@ const credentials = new AwsCredentials(config.aws);
1111

1212
const ResponseObj = require('./../response');
1313

14-
router.all('/', (req, res) => {
15-
const sts = new Aws.STS();
16-
const session = req.session.passport;
14+
module.exports = (app) => {
15+
router.all('/', (req, res) => {
16+
const sts = new Aws.STS();
17+
const session = req.session.passport;
1718

18-
const refreshResponseObj = Object.assign(ResponseObj, {
19-
accountId: session.accountId
20-
});
21-
22-
sts.assumeRoleWithSAML({
23-
PrincipalArn: session.principalArn,
24-
RoleArn: session.roleArn,
25-
SAMLAssertion: session.samlResponse,
26-
DurationSeconds: config.aws.duration
27-
}, (assumeRoleErr, data) => {
28-
if (assumeRoleErr) {
29-
res.redirect(config.auth.entryPoint);
30-
return;
31-
}
32-
33-
const credentialResponseObj = Object.assign(refreshResponseObj, {
34-
accessKey: data.Credentials.AccessKeyId,
35-
secretKey: data.Credentials.SecretAccessKey,
36-
sessionToken: data.Credentials.SessionToken
19+
const refreshResponseObj = Object.assign(ResponseObj, {
20+
accountId: session.accountId
3721
});
3822

39-
res.render('refresh', credentialResponseObj);
23+
sts.assumeRoleWithSAML({
24+
PrincipalArn: session.principalArn,
25+
RoleArn: session.roleArn,
26+
SAMLAssertion: session.samlResponse,
27+
DurationSeconds: config.aws.duration
28+
}, (assumeRoleErr, data) => {
29+
if (assumeRoleErr) {
30+
res.redirect(config.auth.entryPoint);
31+
return;
32+
}
33+
34+
const credentialResponseObj = Object.assign(refreshResponseObj, {
35+
accessKey: data.Credentials.AccessKeyId,
36+
secretKey: data.Credentials.SecretAccessKey,
37+
sessionToken: data.Credentials.SessionToken
38+
});
39+
40+
const profileName = `awsaml-${session.accountId}`;
41+
const metadataUrl = app.get('metadataUrl');
42+
let metadataUrls = Storage.get('metadataUrls');
4043

41-
credentials.save(data.Credentials, `awsaml-${session.accountId}`, (credSaveErr) => {
42-
if (credSaveErr) {
43-
res.render('refresh', Object.assign(credentialResponseObj, {
44-
error: credSaveErr
45-
}));
44+
// If the stored metadataUrl label value is the same as the URL default to the profile name!
45+
if(metadataUrls[metadataUrl] === metadataUrl) {
46+
metadataUrls[metadataUrl] = profileName;
47+
Storage.set('metadataUrls', metadataUrls);
4648
}
49+
res.render('refresh', credentialResponseObj);
50+
51+
credentials.save(data.Credentials, profileName, (credSaveErr) => {
52+
if (credSaveErr) {
53+
res.render('refresh', Object.assign(credentialResponseObj, {
54+
error: credSaveErr
55+
}));
56+
}
57+
});
4758
});
4859
});
49-
});
5060

51-
module.exports = router;
61+
return router;
62+
};

lib/server.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const app = require('./server-config')(auth, config, sessionSecret);
1313
{name: config.auth.path, route: require('./routes/auth')(app, auth)},
1414
{name: '/configure', route: require('./routes/configure')(app, auth)},
1515
{name: '/logout', route: require('./routes/logout')(app)},
16-
{name: '/refresh', route: require('./routes/refresh')}
16+
{name: '/refresh', route: require('./routes/refresh')(app)}
1717
].forEach((el) => {
1818
app.use(el.name, el.route);
1919
});

public/css/app.css

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,11 @@ dd {
5656
.button-margin {
5757
margin-left: 10px;
5858
}
59+
60+
.scrollable-list {
61+
overflow-x: hidden;
62+
}
63+
64+
#recent-logins .scrollable-list {
65+
height: 150px;
66+
}

views/configure.jsx

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ const DefaultLayout = require('./layouts/default');
33
const Error = require('./error');
44

55
const propTypes = {
6+
defaultMetadataUrl: React.PropTypes.string.isRequired,
67
error: React.PropTypes.string,
78
title: React.PropTypes.string.isRequired,
8-
metadataUrl: React.PropTypes.string.isRequired,
9+
metadataUrls: React.PropTypes.object.isRequired,
910
metadataUrlValid: React.PropTypes.bool
1011
};
1112

@@ -44,17 +45,50 @@ class Configure extends React.Component {
4445
<label htmlFor='metadataUrl'>SAML Metadata URL</label>
4546
<input
4647
className='form-control'
47-
defaultValue={this.props.metadataUrl}
48+
defaultValue={this.props.defaultMetadataUrl}
4849
id='metadataUrl'
4950
name='metadataUrl'
5051
pattern='https://.+'
5152
required
5253
type='url'
5354
/>
5455
</div>
56+
5557
<button className='btn btn-default' type='submit'>Done</button>
5658
</fieldset>
5759
</form>
60+
<div id='recent-logins'>
61+
<h4>Recent Logins</h4>
62+
<ul className='list-group scrollable-list' id='recent-logins'>{
63+
Object.keys(this.props.metadataUrls).map((key) => {
64+
const pretty = this.props.metadataUrls[key];
65+
const prettyId = `#${pretty}`;
66+
67+
return (
68+
<li className='list-group-item' key={key}>
69+
<details>
70+
<summary>{pretty}</summary>
71+
72+
<br/>
73+
74+
<div className='input-group'>
75+
<input
76+
className='form-control' defaultValue={key}
77+
id={pretty} readonly // eslint-disable-line react/no-unknown-property
78+
/>
79+
<span className='input-group-btn'>
80+
<button
81+
className='btn btn-default copy-to-clipboard-button'
82+
data-clipboard-target={prettyId}
83+
><span className='glyphicon glyphicon-copy'/></button>
84+
</span>
85+
</div>
86+
</details>
87+
</li>
88+
);
89+
})
90+
}</ul>
91+
</div>
5892
</div>
5993
</div>
6094
</DefaultLayout>

views/layouts/default.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const DefaultLayout = function render(props) {
3232
{props.children}
3333
</div>
3434
</div>
35+
<script src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/1.5.12/clipboard.min.js"></script>
3536
<script src='https://cdnjs.cloudflare.com/ajax/libs/prism/0.0.1/prism.min.js'></script>
3637
</body>
3738
</html>

views/refresh.jsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,6 @@ class Refresh extends React.Component {
7979
);
8080
}
8181
}
82-
8382
Refresh.propTypes = propTypes;
8483
Refresh.displayName = 'Refresh';
8584

0 commit comments

Comments
 (0)