Add SSO support to new user activation (#1504)
Closes #1502. This PR adds support for SSO to the new user creation process. An admin now has the option to select SSO when creating a new user. When the confirmation form is submitted, the user is automatically authenticated with the IDP, and if successful, is redirected to the Kolide home page. Password authentication, password change and password reset are not allowed for an SSO user.
This commit is contained in:
+2
-1
@@ -8,7 +8,8 @@ module.exports = {
|
||||
],
|
||||
env: {
|
||||
'node': true,
|
||||
'mocha': true
|
||||
'mocha': true,
|
||||
'browser': true
|
||||
},
|
||||
globals: {
|
||||
'expect': false,
|
||||
|
||||
@@ -11,12 +11,15 @@ const userActionOptions = (isCurrentUser, user, invite) => {
|
||||
|
||||
if (invite) return inviteActions;
|
||||
|
||||
return [
|
||||
const result = [
|
||||
userEnableAction,
|
||||
userPromotionAction,
|
||||
{ disabled: false, label: 'Require Password Reset', value: 'reset_password' },
|
||||
{ disabled: false, label: 'Modify Details', value: 'modify_details' },
|
||||
];
|
||||
if (!user.sso_enabled) {
|
||||
result.push({ disabled: false, label: 'Require Password Reset', value: 'reset_password' });
|
||||
}
|
||||
result.push({ disabled: false, label: 'Modify Details', value: 'modify_details' });
|
||||
return result;
|
||||
};
|
||||
|
||||
const userStatusLabel = (user, invite) => {
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
|
||||
import Form from 'components/forms/Form';
|
||||
import formFieldInterface from 'interfaces/form_field';
|
||||
import Button from 'components/buttons/Button';
|
||||
import InputFieldWithIcon from 'components/forms/fields/InputFieldWithIcon';
|
||||
import helpers from './helpers';
|
||||
|
||||
const formFields = ['name', 'username', 'password', 'password_confirmation'];
|
||||
const { validate } = helpers;
|
||||
|
||||
class ConfirmSSOInviteForm extends Component {
|
||||
static propTypes = {
|
||||
baseError: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
fields: PropTypes.shape({
|
||||
name: formFieldInterface.isRequired,
|
||||
username: formFieldInterface.isRequired,
|
||||
password: formFieldInterface.isRequired,
|
||||
password_confirmation: formFieldInterface.isRequired,
|
||||
}).isRequired,
|
||||
handleSubmit: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { baseError, className, fields, handleSubmit } = this.props;
|
||||
|
||||
return (
|
||||
<form className={className}>
|
||||
{baseError && <div className="form__base-error">{baseError}</div>}
|
||||
<div className="fields">
|
||||
<InputFieldWithIcon
|
||||
{...fields.name}
|
||||
autofocus
|
||||
placeholder="Full Name"
|
||||
/>
|
||||
<InputFieldWithIcon
|
||||
{...fields.username}
|
||||
iconName="username"
|
||||
placeholder="Username"
|
||||
/>
|
||||
</div>
|
||||
<Button onClick={handleSubmit} type="Submit" variant="gradient">
|
||||
Submit
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Form(ConfirmSSOInviteForm, {
|
||||
fields: formFields,
|
||||
validate,
|
||||
});
|
||||
@@ -0,0 +1,122 @@
|
||||
import React from 'react';
|
||||
import expect, { createSpy, restoreSpies } from 'expect';
|
||||
import { mount } from 'enzyme';
|
||||
import { noop } from 'lodash';
|
||||
|
||||
import ConfirmInviteForm from 'components/forms/ConfirmInviteForm';
|
||||
import { fillInFormInput } from 'test/helpers';
|
||||
|
||||
describe('ConfirmInviteForm - component', () => {
|
||||
afterEach(restoreSpies);
|
||||
|
||||
const handleSubmitSpy = createSpy();
|
||||
const inviteToken = 'abc123';
|
||||
const formData = { invite_token: inviteToken };
|
||||
const form = mount(<ConfirmInviteForm formData={formData} handleSubmit={handleSubmitSpy} />);
|
||||
|
||||
const nameInput = form.find({ name: 'name' }).find('input');
|
||||
const passwordConfirmationInput = form.find({ name: 'password_confirmation' }).find('input');
|
||||
const passwordInput = form.find({ name: 'password' }).find('input');
|
||||
const submitBtn = form.find('button');
|
||||
const usernameInput = form.find({ name: 'username' }).find('input');
|
||||
|
||||
it('renders', () => {
|
||||
expect(form.length).toEqual(1);
|
||||
});
|
||||
|
||||
it('renders the base error', () => {
|
||||
const baseError = 'Unable to authenticate the current user';
|
||||
const formWithError = mount(<ConfirmInviteForm serverErrors={{ base: baseError }} handleSubmit={noop} />);
|
||||
const formWithoutError = mount(<ConfirmInviteForm handleSubmit={noop} />);
|
||||
|
||||
expect(formWithError.text()).toInclude(baseError);
|
||||
expect(formWithoutError.text()).toNotInclude(baseError);
|
||||
});
|
||||
|
||||
it('calls the handleSubmit prop with the invite_token when valid', () => {
|
||||
fillInFormInput(nameInput, 'Gnar Dog');
|
||||
fillInFormInput(usernameInput, 'gnardog');
|
||||
fillInFormInput(passwordInput, 'p@ssw0rd');
|
||||
fillInFormInput(passwordConfirmationInput, 'p@ssw0rd');
|
||||
submitBtn.simulate('click');
|
||||
|
||||
expect(handleSubmitSpy).toHaveBeenCalledWith({
|
||||
...formData,
|
||||
name: 'Gnar Dog',
|
||||
username: 'gnardog',
|
||||
password: 'p@ssw0rd',
|
||||
password_confirmation: 'p@ssw0rd',
|
||||
});
|
||||
});
|
||||
|
||||
describe('name input', () => {
|
||||
it('changes form state on change', () => {
|
||||
fillInFormInput(nameInput, 'Gnar Dog');
|
||||
|
||||
expect(form.state().formData).toInclude({ name: 'Gnar Dog' });
|
||||
});
|
||||
|
||||
it('validates the field must be present', () => {
|
||||
fillInFormInput(nameInput, '');
|
||||
form.find('button').simulate('click');
|
||||
|
||||
expect(form.state().errors).toInclude({ name: 'Full name must be present' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('username input', () => {
|
||||
it('changes form state on change', () => {
|
||||
fillInFormInput(usernameInput, 'gnardog');
|
||||
|
||||
expect(form.state().formData).toInclude({ username: 'gnardog' });
|
||||
});
|
||||
|
||||
it('validates the field must be present', () => {
|
||||
fillInFormInput(usernameInput, '');
|
||||
submitBtn.simulate('click');
|
||||
|
||||
expect(form.state().errors).toInclude({ username: 'Username must be present' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('password input', () => {
|
||||
it('changes form state on change', () => {
|
||||
fillInFormInput(passwordInput, 'p@ssw0rd');
|
||||
|
||||
expect(form.state().formData).toInclude({ password: 'p@ssw0rd' });
|
||||
});
|
||||
|
||||
it('validates the field must be present', () => {
|
||||
fillInFormInput(passwordInput, '');
|
||||
form.find('button').simulate('click');
|
||||
|
||||
expect(form.state().errors).toInclude({ password: 'Password must be present' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('password_confirmation input', () => {
|
||||
it('changes form state on change', () => {
|
||||
fillInFormInput(passwordConfirmationInput, 'p@ssw0rd');
|
||||
|
||||
expect(form.state().formData).toInclude({ password_confirmation: 'p@ssw0rd' });
|
||||
});
|
||||
|
||||
it('validates the password_confirmation matches the password', () => {
|
||||
fillInFormInput(passwordInput, 'p@ssw0rd');
|
||||
fillInFormInput(passwordConfirmationInput, 'another-password');
|
||||
form.find('button').simulate('click');
|
||||
|
||||
expect(form.state().errors).toInclude({
|
||||
password_confirmation: 'Password confirmation does not match password',
|
||||
});
|
||||
});
|
||||
|
||||
it('validates the field must be present', () => {
|
||||
fillInFormInput(passwordConfirmationInput, '');
|
||||
form.find('button').simulate('click');
|
||||
|
||||
expect(form.state().errors).toInclude({ password_confirmation: 'Password confirmation must be present' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { size } from 'lodash';
|
||||
|
||||
const validate = (formData) => {
|
||||
const errors = {};
|
||||
const {
|
||||
name,
|
||||
username,
|
||||
} = formData;
|
||||
|
||||
if (!name) {
|
||||
errors.name = 'Full name must be present';
|
||||
}
|
||||
|
||||
if (!username) {
|
||||
errors.username = 'Username must be present';
|
||||
}
|
||||
|
||||
const valid = !size(errors);
|
||||
|
||||
return { valid, errors };
|
||||
};
|
||||
|
||||
export default { validate };
|
||||
@@ -0,0 +1 @@
|
||||
export default from './ConfirmSSOInviteForm';
|
||||
@@ -18,6 +18,7 @@ class InviteUserForm extends Component {
|
||||
invitedBy: userInterface,
|
||||
onCancel: PropTypes.func,
|
||||
onSubmit: PropTypes.func,
|
||||
canUseSSO: PropTypes.bool,
|
||||
};
|
||||
|
||||
constructor (props) {
|
||||
@@ -28,11 +29,13 @@ class InviteUserForm extends Component {
|
||||
admin: null,
|
||||
email: null,
|
||||
name: null,
|
||||
sso_enabled: null,
|
||||
},
|
||||
formData: {
|
||||
admin: false,
|
||||
email: '',
|
||||
name: '',
|
||||
sso_enabled: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -80,14 +83,14 @@ class InviteUserForm extends Component {
|
||||
const valid = this.validate();
|
||||
|
||||
if (valid) {
|
||||
const { formData: { admin, email, name } } = this.state;
|
||||
const { formData: { admin, email, name, sso_enabled } } = this.state;
|
||||
const { invitedBy, onSubmit } = this.props;
|
||||
|
||||
return onSubmit({
|
||||
admin,
|
||||
email,
|
||||
invited_by: invitedBy.id,
|
||||
name,
|
||||
sso_enabled,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -126,7 +129,7 @@ class InviteUserForm extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { errors, formData: { admin, email, name } } = this.state;
|
||||
const { errors, formData: { admin, email, name, ssoEnabled } } = this.state;
|
||||
const { onCancel, serverErrors } = this.props;
|
||||
const { onFormSubmit, onInputChange, onCheckboxChange } = this;
|
||||
const baseError = serverErrors.base;
|
||||
@@ -162,6 +165,19 @@ class InviteUserForm extends Component {
|
||||
Enable Admin
|
||||
</Checkbox>
|
||||
</div>
|
||||
<div className={`${baseClass}__radio`}>
|
||||
<p className={`${baseClass}__role`}>single sign on</p>
|
||||
<Checkbox
|
||||
name="sso_enabled"
|
||||
onChange={onCheckboxChange('sso_enabled')}
|
||||
value={ssoEnabled}
|
||||
disabled={!this.props.canUseSSO}
|
||||
wrapperClassName={`${baseClass}__invite-admin`}
|
||||
>
|
||||
Enable Single Sign On
|
||||
</Checkbox>
|
||||
</div>
|
||||
|
||||
<div className={`${baseClass}__btn-wrap`}>
|
||||
<Button className={`${baseClass}__btn`} type="submit" variant="brand">
|
||||
Invite
|
||||
|
||||
@@ -37,4 +37,5 @@ export default {
|
||||
UPDATE_USER_ADMIN: (id) => {
|
||||
return `/v1/kolide/users/${id}/admin`;
|
||||
},
|
||||
SSO: '/v1/kolide/sso',
|
||||
};
|
||||
|
||||
@@ -25,5 +25,10 @@ export default (client) => {
|
||||
|
||||
return client.authenticatedPost(endpoint);
|
||||
},
|
||||
initializeSSO: (url) => {
|
||||
const { SSO } = endpoints;
|
||||
const endpoint = client._endpoint(SSO);
|
||||
return Base.post(endpoint, JSON.stringify({ relay_url: url }));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -215,6 +215,7 @@ export class UserManagementPage extends Component {
|
||||
const { currentUser, inviteErrors } = this.props;
|
||||
const { showInviteUserModal } = this.state;
|
||||
const { onInviteCancel, onInviteUserSubmit, toggleInviteUserModal } = this;
|
||||
const ssoEnabledForApp = this.props.config.enable_sso;
|
||||
|
||||
if (!showInviteUserModal) {
|
||||
return false;
|
||||
@@ -231,6 +232,7 @@ export class UserManagementPage extends Component {
|
||||
invitedBy={currentUser}
|
||||
onCancel={onInviteCancel}
|
||||
onSubmit={onInviteUserSubmit}
|
||||
canUseSSO={ssoEnabledForApp}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import AuthenticationFormWrapper from 'components/AuthenticationFormWrapper';
|
||||
import ConfirmSSOInviteForm from 'components/forms/ConfirmSSOInviteForm';
|
||||
import EnsureUnauthenticated from 'components/EnsureUnauthenticated';
|
||||
import userActions from 'redux/nodes/entities/users/actions';
|
||||
import authActions from 'redux/nodes/auth/actions';
|
||||
import paths from 'router/paths';
|
||||
|
||||
|
||||
const baseClass = 'confirm-ssoinvite-page';
|
||||
|
||||
class ConfirmSSOInvitePage extends Component {
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func,
|
||||
inviteFormData: PropTypes.shape({
|
||||
email: PropTypes.string.isRequired,
|
||||
invite_token: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
userErrors: PropTypes.shape({
|
||||
base: PropTypes.string,
|
||||
}),
|
||||
};
|
||||
|
||||
componentWillUnmount () {
|
||||
const { dispatch } = this.props;
|
||||
const { clearErrors } = userActions;
|
||||
|
||||
dispatch(clearErrors());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
onSubmit = (formData) => {
|
||||
const { create } = userActions;
|
||||
const { ssoRedirect } = authActions;
|
||||
const { dispatch } = this.props;
|
||||
const { HOME } = paths;
|
||||
|
||||
formData.sso_invite = true;
|
||||
dispatch(create(formData))
|
||||
.then(() => {
|
||||
// set redirect so that we will get redirected to home page after
|
||||
// the user authenticates with the idp
|
||||
dispatch(ssoRedirect(HOME))
|
||||
.then((result) => {
|
||||
window.location.href = result.payload.ssoRedirectURL;
|
||||
})
|
||||
.catch(() => false);
|
||||
})
|
||||
.catch(() => false);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
render () {
|
||||
const { inviteFormData, userErrors } = this.props;
|
||||
const { onSubmit } = this;
|
||||
|
||||
return (
|
||||
<AuthenticationFormWrapper>
|
||||
<div className={`${baseClass}__lead-wrapper`}>
|
||||
<p className={`${baseClass}__lead-text`}>
|
||||
Welcome to the party, {inviteFormData.email}!
|
||||
</p>
|
||||
<p className={`${baseClass}__sub-lead-text`}>
|
||||
Please take a moment to fill out the following information before we take you into <b>Kolide</b>
|
||||
</p>
|
||||
</div>
|
||||
<div className={`${baseClass}__form-section-wrapper`}>
|
||||
<div className={`${baseClass}__form-section-description`}>
|
||||
<h2>SET USERNAME</h2>
|
||||
</div>
|
||||
<ConfirmSSOInviteForm
|
||||
className={`${baseClass}__form`}
|
||||
formData={inviteFormData}
|
||||
handleSubmit={onSubmit}
|
||||
serverErrors={userErrors}
|
||||
/>
|
||||
</div>
|
||||
</AuthenticationFormWrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, { location: urlLocation, params }) => {
|
||||
const { email, name } = urlLocation.query;
|
||||
const { invite_token: inviteToken } = params;
|
||||
const inviteFormData = { email, invite_token: inviteToken, name };
|
||||
const { errors: userErrors } = state.entities.users;
|
||||
|
||||
return { inviteFormData, userErrors };
|
||||
};
|
||||
|
||||
const ConnectedComponent = connect(mapStateToProps)(ConfirmSSOInvitePage);
|
||||
export default EnsureUnauthenticated(ConnectedComponent);
|
||||
@@ -0,0 +1,40 @@
|
||||
import expect from 'expect';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
import ConfirmInvitePage from 'pages/ConfirmInvitePage';
|
||||
import { connectedComponent, reduxMockStore } from 'test/helpers';
|
||||
|
||||
describe('ConfirmInvitePage - component', () => {
|
||||
const inviteToken = 'abc123';
|
||||
const location = { query: { email: 'hi@gnar.dog', name: 'Gnar Dog' } };
|
||||
const params = { invite_token: inviteToken };
|
||||
const mockStore = reduxMockStore({ auth: {}, entities: { users: {} } });
|
||||
const component = connectedComponent(ConfirmInvitePage, {
|
||||
props: { location, params },
|
||||
mockStore,
|
||||
});
|
||||
const page = mount(component);
|
||||
|
||||
it('renders', () => {
|
||||
expect(page.length).toEqual(1);
|
||||
expect(
|
||||
page.find('ConfirmInvitePage').prop('inviteFormData')
|
||||
).toEqual({
|
||||
email: 'hi@gnar.dog',
|
||||
invite_token: inviteToken,
|
||||
name: 'Gnar Dog',
|
||||
});
|
||||
});
|
||||
|
||||
it('renders a ConfirmInviteForm', () => {
|
||||
expect(page.find('ConfirmInviteForm').length).toEqual(1);
|
||||
});
|
||||
|
||||
it('clears errors on unmount', () => {
|
||||
page.unmount();
|
||||
|
||||
expect(mockStore.getActions()).toInclude({
|
||||
type: 'users_CLEAR_ERRORS',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
.confirm-ssoinvite-page {
|
||||
&__form-section-description {
|
||||
h2 {
|
||||
font-size: 18px;
|
||||
font-weight: $bold;
|
||||
line-height: 1.5;
|
||||
letter-spacing: 0.6px;
|
||||
color: $text-dark;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
}
|
||||
|
||||
p {
|
||||
color: $text-dark;
|
||||
font-size: 14px;
|
||||
font-weight: $light;
|
||||
}
|
||||
}
|
||||
|
||||
&__form-section-wrapper {
|
||||
background-color: $white;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.3);
|
||||
box-sizing: border-box;
|
||||
height: 459px;
|
||||
padding: 35px;
|
||||
width: 500px;
|
||||
margin-bottom: 110px;
|
||||
}
|
||||
|
||||
&__form {
|
||||
width: 440px;
|
||||
|
||||
.fields {
|
||||
background-color: $white;
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
box-shadow: 0 10px 30px 0 rgba(0, 0, 0, 0.3);
|
||||
box-sizing: border-box;
|
||||
padding: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
&__lead-wrapper {
|
||||
background-color: $white;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.3);
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 30px;
|
||||
padding: 35px;
|
||||
width: 500px;
|
||||
}
|
||||
|
||||
&__lead-text {
|
||||
color: $brand;
|
||||
font-size: 18px;
|
||||
font-weight: $light;
|
||||
margin-top: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__sub-lead-text {
|
||||
color: $text-dark;
|
||||
font-size: 14px;
|
||||
font-weight: $light;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export default from './ConfirmSSOInvitePage';
|
||||
@@ -196,7 +196,7 @@ export class UserSettingsPage extends Component {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { admin, updated_at: updatedAt } = user;
|
||||
const { admin, updated_at: updatedAt, sso_enabled: ssoEnabled } = user;
|
||||
const roleText = admin ? 'ADMIN' : 'USER';
|
||||
const lastUpdatedAt = moment(updatedAt).fromNow();
|
||||
|
||||
@@ -228,7 +228,7 @@ export class UserSettingsPage extends Component {
|
||||
<Icon name="lock-big" />
|
||||
<strong>Password</strong>
|
||||
</div>
|
||||
<Button onClick={onShowModal} variant="brand" className={`${baseClass}__button`}>
|
||||
<Button onClick={onShowModal} variant="brand" disabled={ssoEnabled} className={`${baseClass}__button`}>
|
||||
CHANGE PASSWORD
|
||||
</Button>
|
||||
<p className={`${baseClass}__last-updated`}>Last changed: {lastUpdatedAt}</p>
|
||||
|
||||
@@ -3,12 +3,15 @@ export const frontendFormattedConfig = (config) => {
|
||||
org_info: orgInfo,
|
||||
server_settings: serverSettings,
|
||||
smtp_settings: smtpSettings,
|
||||
sso_settings: ssoSettings,
|
||||
|
||||
} = config;
|
||||
|
||||
return {
|
||||
...orgInfo,
|
||||
...serverSettings,
|
||||
...smtpSettings,
|
||||
...ssoSettings,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -21,6 +21,10 @@ export const PERFORM_REQUIRED_PASSWORD_RESET_REQUEST = 'PERFORM_REQUIRED_PASSWOR
|
||||
export const PERFORM_REQUIRED_PASSWORD_RESET_SUCCESS = 'PERFORM_REQUIRED_PASSWORD_RESET_SUCCESS';
|
||||
export const PERFORM_REQUIRED_PASSWORD_RESET_FAILURE = 'PERFORM_REQUIRED_PASSWORD_RESET_FAILURE';
|
||||
|
||||
export const SSO_REDIRECT_REQUEST = 'SSO_REDIRECT_REQUEST';
|
||||
export const SSO_REDIRECT_SUCCESS = 'SSO_REDIRECT_SUCCESS';
|
||||
export const SSO_REDIRECT_FAILURE = 'SSO_REDIRECT_FAILURE';
|
||||
|
||||
export const licenseFailure = (errors) => {
|
||||
return {
|
||||
type: LICENSE_FAILURE,
|
||||
@@ -123,6 +127,38 @@ export const fetchCurrentUser = () => {
|
||||
};
|
||||
};
|
||||
|
||||
export const ssoRedirectRequest = { type: SSO_REDIRECT_REQUEST };
|
||||
export const ssoRedirectSuccess = (redirectURL) => {
|
||||
return {
|
||||
type: SSO_REDIRECT_SUCCESS,
|
||||
payload: {
|
||||
ssoRedirectURL: redirectURL,
|
||||
},
|
||||
};
|
||||
};
|
||||
export const ssoRedirectFailure = ({ errors }) => {
|
||||
return {
|
||||
type: SSO_REDIRECT_FAILURE,
|
||||
payload: {
|
||||
errors,
|
||||
},
|
||||
};
|
||||
};
|
||||
// formData { relay_url: 'some/url'}
|
||||
export const ssoRedirect = (formData) => {
|
||||
return (dispatch) => {
|
||||
dispatch(ssoRedirectRequest);
|
||||
return Kolide.sessions.initializeSSO(formData)
|
||||
.then((response) => {
|
||||
return dispatch(ssoRedirectSuccess(response.url));
|
||||
}).catch((response) => {
|
||||
dispatch(ssoRedirectFailure({ base: 'Unable to authenticate the current user' }));
|
||||
throw response;
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
// formData should be { username: <string>, password: <string> }
|
||||
export const loginUser = (formData) => {
|
||||
return (dispatch) => {
|
||||
@@ -254,3 +290,7 @@ export const performRequiredPasswordReset = (resetParams) => {
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export default {
|
||||
ssoRedirect,
|
||||
};
|
||||
|
||||
@@ -17,13 +17,69 @@ import {
|
||||
PERFORM_REQUIRED_PASSWORD_RESET_REQUEST,
|
||||
PERFORM_REQUIRED_PASSWORD_RESET_FAILURE,
|
||||
PERFORM_REQUIRED_PASSWORD_RESET_SUCCESS,
|
||||
SSO_REDIRECT_REQUEST,
|
||||
SSO_REDIRECT_SUCCESS,
|
||||
updateUser,
|
||||
ssoRedirect,
|
||||
} from './actions';
|
||||
|
||||
const store = { entities: { invites: {}, users: {} } };
|
||||
const user = { ...userStub, id: 1, email: 'zwass@kolide.co', force_password_reset: false };
|
||||
|
||||
describe('Auth - actions', () => {
|
||||
describe('#ssoRedirect', () => {
|
||||
const ssoURL = 'http://salesforce.idp.com';
|
||||
const relayURL = '/';
|
||||
afterEach(restoreSpies);
|
||||
|
||||
describe('successful request', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(Kolide.sessions, 'initializeSSO').andReturn(Promise.resolve({ url: ssoURL }));
|
||||
});
|
||||
|
||||
it('calls the API', () => {
|
||||
const mockStore = reduxMockStore(store);
|
||||
|
||||
mockStore.dispatch(ssoRedirect(relayURL))
|
||||
.then(() => {
|
||||
expect(Kolide.sessions.toHaveBeenCalledWith(relayURL));
|
||||
})
|
||||
.catch(() => {
|
||||
expect(Kolide.sessions.toHaveBeenCalledWith(relayURL));
|
||||
});
|
||||
});
|
||||
|
||||
it('executes to correct actions', () => {
|
||||
const mockStore = reduxMockStore(store);
|
||||
const actions = [
|
||||
{ type: SSO_REDIRECT_REQUEST },
|
||||
{ type: SSO_REDIRECT_SUCCESS,
|
||||
payload: { ssoRedirectURL: ssoURL },
|
||||
},
|
||||
];
|
||||
|
||||
return mockStore.dispatch(ssoRedirect(relayURL))
|
||||
.then(() => {
|
||||
expect(mockStore.getActions()).toEqual(actions);
|
||||
})
|
||||
.catch(() => {
|
||||
expect(mockStore.getActions()).toEqual(actions);
|
||||
});
|
||||
});
|
||||
|
||||
it('retrieves redirect url', () => {
|
||||
const mockStore = reduxMockStore(store);
|
||||
return mockStore.dispatch(ssoRedirect(relayURL))
|
||||
.then((result) => {
|
||||
expect(ssoURL).toEqual(result.payload.ssoRedirectURL);
|
||||
})
|
||||
.catch((result) => {
|
||||
expect(ssoURL).toEqual(result);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#createLicense', () => {
|
||||
const license = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ';
|
||||
afterEach(restoreSpies);
|
||||
|
||||
@@ -15,6 +15,9 @@ import {
|
||||
PERFORM_REQUIRED_PASSWORD_RESET_REQUEST,
|
||||
PERFORM_REQUIRED_PASSWORD_RESET_SUCCESS,
|
||||
PERFORM_REQUIRED_PASSWORD_RESET_FAILURE,
|
||||
SSO_REDIRECT_REQUEST,
|
||||
SSO_REDIRECT_SUCCESS,
|
||||
SSO_REDIRECT_FAILURE,
|
||||
} from './actions';
|
||||
|
||||
export const initialState = {
|
||||
@@ -35,6 +38,7 @@ const reducer = (state = initialState, action) => {
|
||||
case LOGIN_REQUEST:
|
||||
case LOGOUT_REQUEST:
|
||||
case UPDATE_USER_REQUEST:
|
||||
case SSO_REDIRECT_REQUEST:
|
||||
return {
|
||||
...state,
|
||||
loading: true,
|
||||
@@ -51,6 +55,13 @@ const reducer = (state = initialState, action) => {
|
||||
loading: false,
|
||||
user: action.payload.user,
|
||||
};
|
||||
case SSO_REDIRECT_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
loading: false,
|
||||
ssoRedirectURL: action.payload.ssoRedirectURL,
|
||||
};
|
||||
case SSO_REDIRECT_FAILURE:
|
||||
case LICENSE_FAILURE:
|
||||
case LOGIN_FAILURE:
|
||||
return {
|
||||
|
||||
@@ -11,6 +11,7 @@ import AuthenticatedAdminRoutes from 'components/AuthenticatedAdminRoutes';
|
||||
import AuthenticatedRoutes from 'components/AuthenticatedRoutes';
|
||||
import ConfigOptionsPage from 'pages/config/ConfigOptionsPage';
|
||||
import ConfirmInvitePage from 'pages/ConfirmInvitePage';
|
||||
import ConfirmSSOInvitePage from 'pages/ConfirmSSOInvitePage';
|
||||
import CoreLayout from 'layouts/CoreLayout';
|
||||
import EditPackPage from 'pages/packs/EditPackPage';
|
||||
import EmailTokenRedirect from 'components/EmailTokenRedirect';
|
||||
@@ -39,6 +40,7 @@ const routes = (
|
||||
<Route path="license" component={LicensePage} />
|
||||
<Route path="login" component={LoginRoutes}>
|
||||
<Route path="invites/:invite_token" component={ConfirmInvitePage} />
|
||||
<Route path="ssoinvites/:invite_token" component={ConfirmSSOInvitePage} />
|
||||
<Route path="forgot" />
|
||||
<Route path="reset" />
|
||||
</Route>
|
||||
|
||||
@@ -10,11 +10,11 @@ import (
|
||||
|
||||
func testCreateUser(t *testing.T, ds kolide.Datastore) {
|
||||
var createTests = []struct {
|
||||
username, password, email string
|
||||
isAdmin, passwordReset bool
|
||||
username, password, email string
|
||||
isAdmin, passwordReset, sso bool
|
||||
}{
|
||||
{"marpaia", "foobar", "mike@kolide.co", true, false},
|
||||
{"jason", "foobar", "jason@kolide.co", true, false},
|
||||
{"marpaia", "foobar", "mike@kolide.co", true, false, true},
|
||||
{"jason", "foobar", "jason@kolide.co", true, false, false},
|
||||
}
|
||||
|
||||
for _, tt := range createTests {
|
||||
@@ -23,7 +23,8 @@ func testCreateUser(t *testing.T, ds kolide.Datastore) {
|
||||
Password: []byte(tt.password),
|
||||
Admin: tt.isAdmin,
|
||||
AdminForcedPasswordReset: tt.passwordReset,
|
||||
Email: tt.email,
|
||||
Email: tt.email,
|
||||
SSOEnabled: tt.sso,
|
||||
}
|
||||
user, err := ds.NewUser(u)
|
||||
assert.Nil(t, err)
|
||||
@@ -35,6 +36,7 @@ func testCreateUser(t *testing.T, ds kolide.Datastore) {
|
||||
assert.Equal(t, tt.username, verify.Username)
|
||||
assert.Equal(t, tt.email, verify.Email)
|
||||
assert.Equal(t, tt.email, verify.Email)
|
||||
assert.Equal(t, tt.sso, verify.SSOEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,13 +18,13 @@ func (d *Datastore) NewInvite(i *kolide.Invite) (*kolide.Invite, error) {
|
||||
switch err {
|
||||
case nil:
|
||||
sqlStmt = `
|
||||
REPLACE INTO invites ( invited_by, email, admin, name, position, token, deleted)
|
||||
VALUES ( ?, ?, ?, ?, ?, ?, ?)
|
||||
REPLACE INTO invites ( invited_by, email, admin, name, position, token, deleted, sso_enabled)
|
||||
VALUES ( ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`
|
||||
case sql.ErrNoRows:
|
||||
sqlStmt = `
|
||||
INSERT INTO invites ( invited_by, email, admin, name, position, token, deleted)
|
||||
VALUES ( ?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO invites ( invited_by, email, admin, name, position, token, deleted, sso_enabled)
|
||||
VALUES ( ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`
|
||||
default:
|
||||
return nil, errors.Wrap(err, "check for existing invite")
|
||||
@@ -32,7 +32,7 @@ func (d *Datastore) NewInvite(i *kolide.Invite) (*kolide.Invite, error) {
|
||||
|
||||
deleted := false
|
||||
result, err := d.db.Exec(sqlStmt, i.InvitedBy, i.Email, i.Admin,
|
||||
i.Name, i.Position, i.Token, deleted)
|
||||
i.Name, i.Position, i.Token, deleted, i.SSOEnabled)
|
||||
if err != nil && isDuplicate(err) {
|
||||
return nil, alreadyExists("Invite", 0)
|
||||
} else if err != nil {
|
||||
@@ -104,11 +104,11 @@ func (d *Datastore) InviteByToken(token string) (*kolide.Invite, error) {
|
||||
func (d *Datastore) SaveInvite(i *kolide.Invite) error {
|
||||
sql := `
|
||||
UPDATE invites SET invited_by = ?, email = ?, admin = ?,
|
||||
name = ?, position = ?, token = ?
|
||||
name = ?, position = ?, token = ?, sso_enabled = ?
|
||||
WHERE id = ? AND NOT deleted
|
||||
`
|
||||
results, err := d.db.Exec(sql, i.InvitedBy, i.Email,
|
||||
i.Admin, i.Name, i.Position, i.Token, i.ID,
|
||||
i.Admin, i.Name, i.Position, i.Token, i.SSOEnabled, i.ID,
|
||||
)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "save invite")
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package tables
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
func init() {
|
||||
MigrationClient.AddMigration(Up_20170504130602, Down_20170504130602)
|
||||
}
|
||||
|
||||
func Up_20170504130602(tx *sql.Tx) error {
|
||||
_, err := tx.Exec("ALTER TABLE `kolide`.`invites` ADD COLUMN `sso_enabled` TINYINT(1) NOT NULL DEFAULT FALSE AFTER `token`;")
|
||||
return err
|
||||
}
|
||||
|
||||
func Down_20170504130602(tx *sql.Tx) error {
|
||||
_, err := tx.Exec("ALTER TABLE `kolide`.`invites` DROP COLUMN `sso_enabled`;")
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package tables
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
func init() {
|
||||
MigrationClient.AddMigration(Up_20170509132100, Down_20170509132100)
|
||||
}
|
||||
|
||||
func Up_20170509132100(tx *sql.Tx) error {
|
||||
_, err := tx.Exec("ALTER TABLE `kolide`.`users` ADD COLUMN `sso_enabled` TINYINT NOT NULL DEFAULT FALSE AFTER `position`;")
|
||||
return err
|
||||
}
|
||||
|
||||
func Down_20170509132100(tx *sql.Tx) error {
|
||||
_, err := tx.Exec("ALTER TABLE `kolide`.`users` DROP COLUMN `sso_enabled` ;")
|
||||
return err
|
||||
}
|
||||
@@ -11,22 +11,23 @@ import (
|
||||
// NewUser creates a new user
|
||||
func (d *Datastore) NewUser(user *kolide.User) (*kolide.User, error) {
|
||||
sqlStatement := `
|
||||
INSERT INTO users (
|
||||
password,
|
||||
salt,
|
||||
name,
|
||||
username,
|
||||
email,
|
||||
admin,
|
||||
enabled,
|
||||
admin_forced_password_reset,
|
||||
gravatar_url,
|
||||
position
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?)
|
||||
`
|
||||
INSERT INTO users (
|
||||
password,
|
||||
salt,
|
||||
name,
|
||||
username,
|
||||
email,
|
||||
admin,
|
||||
enabled,
|
||||
admin_forced_password_reset,
|
||||
gravatar_url,
|
||||
position,
|
||||
sso_enabled
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?)
|
||||
`
|
||||
result, err := d.db.Exec(sqlStatement, user.Password, user.Salt, user.Name,
|
||||
user.Username, user.Email, user.Admin, user.Enabled,
|
||||
user.AdminForcedPasswordReset, user.GravatarURL, user.Position)
|
||||
user.AdminForcedPasswordReset, user.GravatarURL, user.Position, user.SSOEnabled)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "create new user")
|
||||
}
|
||||
@@ -88,22 +89,23 @@ func (d *Datastore) UserByID(id uint) (*kolide.User, error) {
|
||||
|
||||
func (d *Datastore) SaveUser(user *kolide.User) error {
|
||||
sqlStatement := `
|
||||
UPDATE users SET
|
||||
username = ?,
|
||||
password = ?,
|
||||
salt = ?,
|
||||
name = ?,
|
||||
email = ?,
|
||||
admin = ?,
|
||||
enabled = ?,
|
||||
admin_forced_password_reset = ?,
|
||||
gravatar_url = ?,
|
||||
position = ?
|
||||
WHERE id = ?
|
||||
`
|
||||
UPDATE users SET
|
||||
username = ?,
|
||||
password = ?,
|
||||
salt = ?,
|
||||
name = ?,
|
||||
email = ?,
|
||||
admin = ?,
|
||||
enabled = ?,
|
||||
admin_forced_password_reset = ?,
|
||||
gravatar_url = ?,
|
||||
position = ?,
|
||||
sso_enabled = ?
|
||||
WHERE id = ?
|
||||
`
|
||||
result, err := d.db.Exec(sqlStatement, user.Username, user.Password,
|
||||
user.Salt, user.Name, user.Email, user.Admin, user.Enabled,
|
||||
user.AdminForcedPasswordReset, user.GravatarURL, user.Position, user.ID)
|
||||
user.AdminForcedPasswordReset, user.GravatarURL, user.Position, user.SSOEnabled, user.ID)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "save user")
|
||||
}
|
||||
|
||||
+14
-12
@@ -50,24 +50,26 @@ type InviteService interface {
|
||||
|
||||
// InvitePayload contains fields required to create a new user invite.
|
||||
type InvitePayload struct {
|
||||
InvitedBy *uint `json:"invited_by"`
|
||||
Email *string
|
||||
Admin *bool
|
||||
Name *string
|
||||
Position *string
|
||||
InvitedBy *uint `json:"invited_by"`
|
||||
Email *string
|
||||
Admin *bool
|
||||
Name *string
|
||||
Position *string
|
||||
SSOEnabled *bool `json:"sso_enabled"`
|
||||
}
|
||||
|
||||
// Invite represents an invitation for a user to join Kolide.
|
||||
type Invite struct {
|
||||
UpdateCreateTimestamps
|
||||
DeleteFields
|
||||
ID uint `json:"id"`
|
||||
InvitedBy uint `json:"invited_by" db:"invited_by"`
|
||||
Email string `json:"email"`
|
||||
Admin bool `json:"admin"`
|
||||
Name string `json:"name"`
|
||||
Position string `json:"position,omitempty"`
|
||||
Token string `json:"-"`
|
||||
ID uint `json:"id"`
|
||||
InvitedBy uint `json:"invited_by" db:"invited_by"`
|
||||
Email string `json:"email"`
|
||||
Admin bool `json:"admin"`
|
||||
Name string `json:"name"`
|
||||
Position string `json:"position,omitempty"`
|
||||
Token string `json:"-"`
|
||||
SSOEnabled bool `json:"sso_enabled" db:"sso_enabled"`
|
||||
}
|
||||
|
||||
// InviteMailer is used to build an email template for the invite email.
|
||||
|
||||
@@ -101,6 +101,8 @@ type User struct {
|
||||
AdminForcedPasswordReset bool `json:"force_password_reset" db:"admin_forced_password_reset"`
|
||||
GravatarURL string `json:"gravatar_url" db:"gravatar_url"`
|
||||
Position string `json:"position,omitempty"` // job role
|
||||
// SSOEnabled if true, the single siqn on is used to log in
|
||||
SSOEnabled bool `json:"sso_enabled" db:"sso_enabled"`
|
||||
}
|
||||
|
||||
// UserPayload is used to modify an existing user
|
||||
@@ -114,11 +116,11 @@ type UserPayload struct {
|
||||
GravatarURL *string `json:"gravatar_url"`
|
||||
Position *string `json:"position"`
|
||||
InviteToken *string `json:"invite_token"`
|
||||
SSOInvite *bool `json:"sso_invite"`
|
||||
}
|
||||
|
||||
// User creates a user from payload.
|
||||
func (p UserPayload) User(keySize, cost int) (*User, error) {
|
||||
|
||||
user := &User{
|
||||
Username: *p.Username,
|
||||
Email: *p.Email,
|
||||
|
||||
@@ -58,7 +58,11 @@
|
||||
<table bgcolor="#f4f6fb" height="100px" cellpadding="20px">
|
||||
<tr>
|
||||
<td style="font-family: 'Oxygen', Arial, sans-serif;">
|
||||
<a href="{{.KolideServerURL}}/login/invites/{{.Token}}?name={{.Name}}&email={{.Email}}">{{.KolideServerURL}}/login/invites/{{.Token}}?name={{.Name}}&email={{.Email}}</a>
|
||||
{{if .SSOEnabled}}
|
||||
<a href="{{.KolideServerURL}}/login/ssoinvites/{{.Token}}?name={{.Name}}&email={{.Email}}">{{.KolideServerURL}}/login/ssoinvites/{{.Token}}?name={{.Name}}&email={{.Email}}</a>
|
||||
{{else}}
|
||||
<a href="{{.KolideServerURL}}/login/invites/{{.Token}}?name={{.Name}}&email={{.Email}}">{{.KolideServerURL}}/login/invites/{{.Token}}?name={{.Name}}&email={{.Email}}</a>
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -43,6 +43,9 @@ func (svc service) InviteNewUser(ctx context.Context, payload kolide.InvitePaylo
|
||||
if payload.Name != nil {
|
||||
invite.Name = *payload.Name
|
||||
}
|
||||
if payload.SSOEnabled != nil {
|
||||
invite.SSOEnabled = *payload.SSOEnabled
|
||||
}
|
||||
|
||||
invite, err = svc.ds.NewInvite(invite)
|
||||
if err != nil {
|
||||
|
||||
@@ -87,6 +87,10 @@ func (svc service) CallbackSSO(ctx context.Context, auth kolide.Auth) (*kolide.S
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "finding user in sso callback")
|
||||
}
|
||||
// if user is not active they are not authorized to use the application
|
||||
if !user.Enabled || user.Deleted {
|
||||
return nil, errors.New("user authorization failed")
|
||||
}
|
||||
token, err := svc.makeSession(user.ID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "making user session in sso callback")
|
||||
@@ -112,6 +116,10 @@ func (svc service) Login(ctx context.Context, username, password string) (*kolid
|
||||
if !user.Enabled {
|
||||
return nil, "", authError{reason: "account disabled", clientReason: "account disabled"}
|
||||
}
|
||||
if user.SSOEnabled {
|
||||
const errMessage = "password login not allowed for single sign on users"
|
||||
return nil, "", authError{reason: errMessage, clientReason: errMessage}
|
||||
}
|
||||
if err = user.ValidatePassword(password); err != nil {
|
||||
return nil, "", authError{reason: "bad password"}
|
||||
}
|
||||
|
||||
@@ -38,10 +38,21 @@ func (svc service) NewAdminCreatedUser(ctx context.Context, p kolide.UserPayload
|
||||
}
|
||||
|
||||
func (svc service) newUser(p kolide.UserPayload) (*kolide.User, error) {
|
||||
var ssoEnabled bool
|
||||
// if user is SSO generate a fake password
|
||||
if p.SSOInvite != nil && *p.SSOInvite == true {
|
||||
fakePassword, err := generateRandomText(14)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.Password = &fakePassword
|
||||
ssoEnabled = true
|
||||
}
|
||||
user, err := p.User(svc.config.Auth.SaltKeySize, svc.config.Auth.BcryptCost)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user.SSOEnabled = ssoEnabled
|
||||
user, err = svc.ds.NewUser(user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -192,7 +203,9 @@ func (svc service) setNewPassword(ctx context.Context, user *kolide.User, passwo
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "setting new password")
|
||||
}
|
||||
|
||||
if user.SSOEnabled {
|
||||
return errors.New("set password for single sign on user not allowed")
|
||||
}
|
||||
err = svc.saveUser(user)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "saving changed password")
|
||||
@@ -206,7 +219,9 @@ func (svc service) ChangePassword(ctx context.Context, oldPass, newPass string)
|
||||
if !ok {
|
||||
return errNoContext
|
||||
}
|
||||
|
||||
if vc.User.SSOEnabled {
|
||||
return errors.New("change password for single sign on user not allowed")
|
||||
}
|
||||
if err := vc.User.ValidatePassword(newPass); err == nil {
|
||||
return newInvalidArgumentError("new_password", "cannot reuse old password")
|
||||
}
|
||||
@@ -230,6 +245,9 @@ func (svc service) ResetPassword(ctx context.Context, token, password string) er
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "retrieving user")
|
||||
}
|
||||
if user.SSOEnabled {
|
||||
return errors.New("password reset for single sign on user not allowed")
|
||||
}
|
||||
|
||||
// prevent setting the same password
|
||||
if err := user.ValidatePassword(password); err == nil {
|
||||
@@ -261,7 +279,9 @@ func (svc service) PerformRequiredPasswordReset(ctx context.Context, password st
|
||||
return nil, errNoContext
|
||||
}
|
||||
user := vc.User
|
||||
|
||||
if user.SSOEnabled {
|
||||
return nil, errors.New("password reset for single sign on user not allowed")
|
||||
}
|
||||
if !user.AdminForcedPasswordReset {
|
||||
return nil, errors.New("user does not require password reset")
|
||||
}
|
||||
@@ -288,7 +308,9 @@ func (svc service) RequirePasswordReset(ctx context.Context, uid uint, require b
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "loading user by ID")
|
||||
}
|
||||
|
||||
if user.SSOEnabled {
|
||||
return nil, errors.New("password reset for single sign on user not allowed")
|
||||
}
|
||||
// Require reset on next login
|
||||
user.AdminForcedPasswordReset = require
|
||||
if err := svc.saveUser(user); err != nil {
|
||||
@@ -310,6 +332,9 @@ func (svc service) RequestPasswordReset(ctx context.Context, email string) error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if user.SSOEnabled {
|
||||
return errors.New("password reset for single sign on user not allowed")
|
||||
}
|
||||
|
||||
random, err := kolide.RandomText(svc.config.App.TokenKeySize)
|
||||
if err != nil {
|
||||
|
||||
@@ -24,14 +24,17 @@ func (mw validationMiddleware) NewUser(ctx context.Context, p kolide.UserPayload
|
||||
}
|
||||
}
|
||||
|
||||
if p.Password == nil {
|
||||
invalid.Append("password", "missing required argument")
|
||||
} else {
|
||||
if *p.Password == "" {
|
||||
invalid.Append("password", "cannot be empty")
|
||||
}
|
||||
if err := validatePasswordRequirements(*p.Password); err != nil {
|
||||
invalid.Append("password", err.Error())
|
||||
// we don't need a password for single sign on
|
||||
if p.SSOInvite == nil || *p.SSOInvite == false {
|
||||
if p.Password == nil {
|
||||
invalid.Append("password", "missing required argument")
|
||||
} else {
|
||||
if *p.Password == "" {
|
||||
invalid.Append("password", "cannot be empty")
|
||||
}
|
||||
if err := validatePasswordRequirements(*p.Password); err != nil {
|
||||
invalid.Append("password", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user