Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add multiple role support #119

Merged
merged 21 commits into from
Aug 8, 2018
Merged

Add multiple role support #119

merged 21 commits into from
Aug 8, 2018

Conversation

bturner-r7
Copy link
Contributor

This adds a role switcher when multiple roles are passed via the SAML assertion. This is similar to how the AWS console handles multiple roles. When a single role is passed, the behavior is unchanged.

awsaml-role-switcher

This also includes a few random fixes/improvements that I found along the way:

  • A copy button for the terminal copy/pasta text
  • Display credential data as a grid
  • Fix the bugs and URLs when running in react developer mode

Note to reviewers:
I am not a react or express developer, much less a react+redux+express+electron developer. Please keep this in mind when reviewing and feel free to suggest stylistic changes.

Note to Okta users:

AWS requires that multiple roles are passed as multiple values to the https://aws.amazon.com/SAML/Attributes/Role attribute (see the AWS docs). By default, Okta passes multiple values as a single value separated by commas. To fix this, you must contact Okta support and ask them to enable the SAML_SUPPORT_ARRAY_ATTRIBUTES feature flag. See this post.

Once this feature flag is enabled, you can add specify multiple roles using an Okta expression:

Arrays.flatten(
  "<role_arn>,<principal_arn>",
  "<role_arn>,<principal_arn>",
  "<role_arn>,<principal_arn>"
)

The `https://aws.amazon.com/SAML/Attributes/Role` attribute supports
single or multiple values.  This fixes Awsaml to work when a
multiple-value attribute is passed.

Currently, the first role is always used.  Support for selecting from
the roles may be added later.

GH-105
This appears to not be needed and gets in the way when using react dev
mode.
This adds a role switcher UI when multiple roles,saml-provider pairs are
included in the SAML assertion.  If only one role is present in the SAML
assertion, the switcher is not displayed.

GH-105
@coveralls
Copy link

coveralls commented Aug 5, 2018

Coverage Status

Coverage remained the same at 90.625% when pulling bff6321 on multiple-role-support into 5f45c37 on master.


if (status === 'selected') {
return <Redirect to="/refresh" />;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the correct way to navigate to a different page after an action? This navigates to the refresh page after selecting a role. Doing this from a render method didn't feel natural to me. Is there a better way?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is how react-router works, to redirect you render the redirect component. 🤷‍♂️

To avoid clutter, the profile name is only displayed if it has been
customized.
@@ -43,6 +43,7 @@ class Configure extends Component {
this.state = {
auth: (params['?auth'] && params['?auth'] === 'true'),
loaded: false,
selectRole: (params['?select-role'] && params['?select-role'] === 'true'),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it possible for select-role to be passed as a query parameter when auth is passed? assuming not 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not currently.

}

return (
<RenderIfLoaded isLoaded={this.state.loaded}>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yikes, we should look at having skeleton content (instead of rendering based on async wait), but this is 👌 based on current setup.

Copy link
Contributor

@dgreene-r7 dgreene-r7 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The few comments I had around formatting components can be addressed with the following patches:

diff --git i/src/containers/refresh/Credentials.js w/src/containers/refresh/Credentials.js
index b9263fa..dbf2c20 100644
--- i/src/containers/refresh/Credentials.js
+++ w/src/containers/refresh/Credentials.js
@@ -16,13 +16,19 @@ const CredProps = styled.dl`
 
 const CredPropsKey = styled.dt`
   grid-column: 1;
-  margin-right: .5rem;
+  margin-right: 1rem;
+  margin-bottom: 10px;
+  line-height: 2.5rem;
 `;
 
 const CredPropsVal = styled.dd`
   grid-column: 2;
 `;
 
+const SmallMarginCardBody = styled(CardBody)`
+  padding: 1.25rem 0.5rem;
+`;
+
 export const Credentials = ({awsAccessKey, awsSecretKey, awsSessionToken}) => {
   const creds = new Map();
 
@@ -42,7 +48,7 @@ export const Credentials = ({awsAccessKey, awsSecretKey, awsSessionToken}) => {
     <details>
       <summary>Credentials</summary>
       <Card>
-        <CardBody className="bg-light">
+        <SmallMarginCardBody className="bg-light">
           <CredProps>
             {
               Array.from(creds).map(([name, value]) => {
@@ -61,7 +67,7 @@ export const Credentials = ({awsAccessKey, awsSecretKey, awsSessionToken}) => {
               })
             }
           </CredProps>
-        </CardBody>
+        </SmallMarginCardBody>
       </Card>
     </details>
   );
diff --git i/src/containers/refresh/Refresh.js w/src/containers/refresh/Refresh.js
index f583afd..c42126f 100644
--- i/src/containers/refresh/Refresh.js
+++ w/src/containers/refresh/Refresh.js
@@ -39,6 +39,8 @@ const AccountProps = styled.dl`
   grid-template-columns: auto 1fr;
   margin: 0;
   padding: .5rem;
+  font-family: Consolas,monospace;
+  font-size: 1rem;
 `;
 
 const AccountPropsKey = styled.dt`

roleAttr = [roleAttr];
}

let roles = roleAttr.map((attr, i) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be a const. let is mutable.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking a little further down you could even just set it to session.roles and key off that value when you check the length.

}

let roles = roleAttr.map((attr, i) => {
let arns = attr.split(',');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const as well.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to gracefully handle a case in which the value from the https://aws.amazon.com/SAML/Attributes/Role attribute isn't formatted like:

arn:aws:iam:role1,arn:aws:iam:provider
arn:aws:iam:role2,arn:aws:iam:provider
arn:aws:iam:role3,arn:aws:iam:provider

If so, do you think it makes sense to introduce some sort of arn parsing mechanism so that you can definitively know if any value in that attribute is formatted correctly and, if not, throw it away?

let arns = attr.split(',');

return {
accountId: arns[0].split(':')[4],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This look pretty brittle. Any way you can add some validation to make sure it doesn't return undefined if the arns value isn't structured correctly?

index: i,
principalArn: arns[1],
roleArn: arns[0],
roleName: arns[0].split(':')[5].replace('role/', ''),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above re: brittle-ness.

As a suggestion, consider something like

const [roleArn, principalArn] = arns;
const roleArnArray = roleArn.split(':');
const accountId = roleArnArray[4];
const roleName = roleArnArray[5];

return {
  accountId,
  index: i,
  principalArn,
  roleArn,
  roleName,
};

// the latest roles from the current SAML assertion. If it
// doesn't match, wipe it from the session.
if (session.roleArn && session.principalArn) {
let found = roles.find((role) =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const

<pre className={getLang(platform)}>
{getEnvVars(this.props)}
</pre>
<InputGroupWithCopyButton
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love that you got the copy button working here but is there a way to maintain the pre formatting?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What formatting are you looking for. Are you actually looking for wrapping this text in <pre></pre>? Or are you looking for dark background and monospace font?

The current copy/paste stuff works with <textarea>, not <pre> so I don't think I can (easily) switch to pre. But I can get the dark background and monospace font if that is the goal.

<pre className="card-text language-markup">
<code>{accountId}</code>
</pre>
<AccountProps className="bg-dark text-light">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way to maintain the pre formatting here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not technically with <pre> since I'm now laying this out with a grid layout. Personally I prefer non-monospaced font here since this isn't code or anything like that. I'm leaving as is. Hope that is ok.

submitSelectRole: bindActionCreators(submitSelectRole, dispatch),
});

export const Role = connect(mapStateToProps, mapDispatchToProps)(RoleComponent);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you don't need mapStateToProps you can just pass null to the connect function instead of a function that does nothing.

router.get('/', (req, res) => {
const session = req.session.passport;

if (session === undefined) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be if (!session) {


const axiosClient = axios.create({
baseURL: ENDPOINTS.electron,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this still work correctly without the baseURL option pointing to the backend? If I remember correctly I had some issues with it looking like it worked when testing but it failing using a built version.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a built version, everything is port 2600 so it works. I've tested this.

I may be doing it wrong, but in a dev environment I run these two commands in separate terminals:

  • yarn electron-dev (runs on port 2600)
  • yarn react-start (runs on port 3000, and proxies non-react stuff to port 2600)

In this case, everything should go through port 3000. Previously, some stuff was going to port 2600 and some to port 3000 and this messed up some CORS stuff (I think).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely worth a shot. If we build and release and see bugs here we can always roll this back.

Copy link
Contributor

@dgreene-r7 dgreene-r7 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. My feedback is mostly stylistic at this point and revolves around using forEach vs map.

if (profileName && storedMetadataUrl.url === metadataUrl && storedMetadataUrl.name !== profileName) {
storedMetadataUrl.name = profileName;
if (profile.url !== metadataUrl) {
return res.status(422).json(Object.assign({}, ResponseObj, {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to return an error here or update the profile.url?

Copy link
Contributor Author

@bturner-r7 bturner-r7 Aug 8, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My thoughts are this should be an impossible state to get to. When you press the "Login" button now, it submits the metadata URL and the UUID for the profile. The metadata url box is not editable in the current UI. If the submitted metadata URL doesn't match the stored metadata URL (found using the uuid) then I assume corrupt input and throw an error.

If we ever update the UI to allow changing the metadata URL for existing entries, we can change this.


return storedMetadataUrl;
// Update the stored profile with account number(s) and profile names
metadataUrls.forEach((metadata) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a huge fan of using forEach as it tends to lead to mutation and side-effects. However, I recognize that's a stylistic issue and just something I want to call out here for future reference.


const axiosClient = axios.create({
baseURL: ENDPOINTS.electron,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely worth a shot. If we build and release and see bugs here we can always roll this back.

@bturner-r7 bturner-r7 merged commit 3dc5ed9 into master Aug 8, 2018
@bturner-r7 bturner-r7 deleted the multiple-role-support branch August 8, 2018 18:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants