-
Notifications
You must be signed in to change notification settings - Fork 51
fix(PM-4393) restrict unauthorized access to project url #1744
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
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -411,7 +411,9 @@ class ChallengeList extends Component { | |
| const isMemberOfActiveProject = activeProject && activeProject.members && activeProject.members.some(member => member.userId === loginUserId) | ||
|
|
||
| if (warnMessage) { | ||
| return <Message warnMessage={warnMessage} /> | ||
| const isLinkComponent = warnMessage.includes('{linkComponent}') | ||
| const message = isLinkComponent ? warnMessage.replace('{linkComponent}', '') : warnMessage | ||
| return <Message warnMessage={message} linkComponent={isLinkComponent ? <a href="mailto:support@topcoder.com">support@topcoder.com</a> : null} /> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [💡 |
||
| } | ||
|
|
||
| const statusOptions = _.map(CHALLENGE_STATUS, item => ({ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,20 +5,23 @@ import React from 'react' | |
| import PropTypes from 'prop-types' | ||
| import styles from './Message.module.scss' | ||
|
|
||
| const Message = ({ warnMessage }) => { | ||
| const Message = ({ warnMessage, linkComponent }) => { | ||
| return ( | ||
| <div className={styles.messageContainer}> | ||
| <p>{warnMessage}</p> | ||
| {warnMessage} | ||
| {linkComponent} | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [❗❗ |
||
| </div> | ||
| ) | ||
| } | ||
|
|
||
| Message.defaultProps = { | ||
| warnMessage: '' | ||
| warnMessage: '', | ||
| linkComponent: () => null | ||
| } | ||
|
|
||
| Message.propTypes = { | ||
| warnMessage: PropTypes.string | ||
| warnMessage: PropTypes.string, | ||
| linkComponent: PropTypes.func | ||
| } | ||
|
|
||
| export default Message | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -488,6 +488,8 @@ export const MESSAGE = { | |
| MARK_COMPLETE: 'This will close the task and generate a payment for the assignee and copilot.' | ||
| } | ||
|
|
||
| export const PROJECT_ACCESS_DENIED_MESSAGE = 'You don’t have access to this project. Please contact {linkComponent}' | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [💡 |
||
|
|
||
| /** | ||
| * Challenge cancel reasons | ||
| */ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,33 +14,43 @@ import { | |
| deleteChallenge, | ||
| loadChallengeTypes | ||
| } from '../../actions/challenges' | ||
| import { loadProject, loadProjects, updateProject } from '../../actions/projects' | ||
| import { loadProject, loadProjects, updateProject, clearProjectDetail } from '../../actions/projects' | ||
| import { | ||
| loadNextProjects, | ||
| setActiveProject, | ||
| resetSidebarActiveParams | ||
| } from '../../actions/sidebar' | ||
| import { checkAdmin, checkIsUserInvitedToProject } from '../../util/tc' | ||
| import { withRouter } from 'react-router-dom' | ||
| import { PROJECT_ACCESS_DENIED_MESSAGE } from '../../config/constants' | ||
|
|
||
| class Challenges extends Component { | ||
| constructor (props) { | ||
| super(props) | ||
| this.state = { | ||
| onlyMyProjects: true | ||
| onlyMyProjects: true, | ||
| projectAccessDenied: false | ||
| } | ||
| } | ||
|
|
||
| componentDidMount () { | ||
| async componentDidMount () { | ||
| const { | ||
| dashboard, | ||
| activeProjectId, | ||
| resetSidebarActiveParams, | ||
| menu, | ||
| projectId, | ||
| selfService, | ||
| loadChallengeTypes | ||
| loadChallengeTypes, | ||
| warnMessage | ||
| } = this.props | ||
|
|
||
| // If we were rendered specifically to display a validation/warning message, | ||
| // do not load any project/challenge data. | ||
| if (warnMessage) { | ||
| return | ||
| } | ||
|
|
||
| loadChallengeTypes() | ||
| if (dashboard) { | ||
| this.props.loadProjects('', {}) | ||
|
|
@@ -51,13 +61,46 @@ class Challenges extends Component { | |
| } else if (projectId || selfService) { | ||
| if (projectId && projectId !== -1) { | ||
| window.localStorage.setItem('projectLoading', 'true') | ||
| this.props.loadProject(projectId) | ||
|
|
||
| // For direct `/projects/:projectId/*` navigation, block unauthorized users. | ||
| if (menu !== 'NULL') { | ||
| try { | ||
| await this.props.loadProject(projectId) | ||
| } catch (error) { | ||
| const responseStatus = _.get( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| error, | ||
| 'payload.response.status', | ||
| _.get(error, 'response.status') | ||
| ) | ||
|
|
||
| if (`${responseStatus}` === '403') { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [💡 |
||
| this.setState({ projectAccessDenied: true }) | ||
| this.props.clearProjectDetail() | ||
| window.localStorage.removeItem('projectLoading') | ||
| return | ||
| } | ||
| } | ||
| } else { | ||
| this.props.loadProject(projectId) | ||
| } | ||
| } | ||
|
|
||
| // Only load challenge listing after successful project resolution. | ||
| if (!this.state.projectAccessDenied) { | ||
| this.reloadChallenges(this.props, true) | ||
| } | ||
| this.reloadChallenges(this.props, true) | ||
| } | ||
| } | ||
|
|
||
| componentDidUpdate () { | ||
| if (this.state.projectAccessDenied) { | ||
| return | ||
| } | ||
|
|
||
| if (this.props.warnMessage) { | ||
| return | ||
| } | ||
|
|
||
| const { auth } = this.props | ||
|
|
||
| if (checkIsUserInvitedToProject(auth.token, this.props.projectDetail)) { | ||
|
|
@@ -66,6 +109,9 @@ class Challenges extends Component { | |
| } | ||
|
|
||
| componentWillReceiveProps (nextProps) { | ||
| if (this.state.projectAccessDenied) { | ||
| return | ||
| } | ||
| if ( | ||
| (nextProps.dashboard && this.props.dashboard !== nextProps.dashboard) || | ||
| this.props.activeProjectId !== nextProps.activeProjectId | ||
|
|
@@ -149,19 +195,24 @@ class Challenges extends Component { | |
| metadata, | ||
| fetchNextProjects | ||
| } = this.props | ||
|
|
||
| const { projectAccessDenied } = this.state | ||
| const effectiveWarnMessage = projectAccessDenied | ||
| ? PROJECT_ACCESS_DENIED_MESSAGE | ||
| : warnMessage | ||
| const { challengeTypes = [] } = metadata | ||
| const isActiveProjectLoaded = | ||
| reduxProjectInfo && `${reduxProjectInfo.id}` === `${activeProjectId}` | ||
|
|
||
| return ( | ||
| <Fragment> | ||
| {(dashboard || activeProjectId !== -1 || selfService) && ( | ||
| {(dashboard || activeProjectId !== -1 || selfService || effectiveWarnMessage) && ( | ||
| <ChallengesComponent | ||
| activeProject={{ | ||
| ...(isActiveProjectLoaded ? reduxProjectInfo : {}) | ||
| }} | ||
| fetchNextProjects={fetchNextProjects} | ||
| warnMessage={warnMessage} | ||
| warnMessage={effectiveWarnMessage} | ||
| setActiveProject={setActiveProject} | ||
| dashboard={dashboard} | ||
| challenges={challenges} | ||
|
|
@@ -279,7 +330,8 @@ const mapDispatchToProps = { | |
| setActiveProject, | ||
| partiallyUpdateChallengeDetails, | ||
| deleteChallenge, | ||
| loadProjects | ||
| loadProjects, | ||
| clearProjectDetail | ||
| } | ||
|
|
||
| export default withRouter( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,9 @@ import _ from 'lodash' | |
| import Loader from '../../components/Loader' | ||
| import { loadOnlyProjectInfo } from '../../actions/projects' | ||
| import { checkIsUserInvitedToProject } from '../../util/tc' | ||
| import Challenges from '../Challenges' | ||
| import { clearProjectDetail } from '../../actions/projects' | ||
| import { PROJECT_ACCESS_DENIED_MESSAGE } from '../../config/constants' | ||
|
|
||
| /** | ||
| * Resolves the correct project landing route for `/projects/:projectId`. | ||
|
|
@@ -18,12 +21,14 @@ const ProjectEntry = ({ | |
| history, | ||
| isProjectLoading, | ||
| loadOnlyProjectInfo, | ||
| clearProjectDetail, | ||
| match, | ||
| projectDetail, | ||
| token | ||
| }) => { | ||
| const projectId = _.get(match, 'params.projectId') | ||
| const [resolvedProjectId, setResolvedProjectId] = useState(null) | ||
| const [projectAccessDenied, setProjectAccessDenied] = useState(false) | ||
|
|
||
| useEffect(() => { | ||
| let isActive = true | ||
|
|
@@ -34,22 +39,33 @@ const ProjectEntry = ({ | |
| } | ||
|
|
||
| setResolvedProjectId(null) | ||
| setProjectAccessDenied(false) | ||
| loadOnlyProjectInfo(projectId) | ||
| .then(() => { | ||
| if (isActive) { | ||
| setResolvedProjectId(projectId) | ||
| } | ||
| }) | ||
| .catch(() => { | ||
| .catch((error) => { | ||
| if (isActive) { | ||
| history.replace('/projects') | ||
| const responseStatus = _.get( | ||
| error, | ||
| 'payload.response.status', | ||
| _.get(error, 'response.status') | ||
| ) | ||
| if (`${responseStatus}` === '403') { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| clearProjectDetail() | ||
| setProjectAccessDenied(true) | ||
| } else { | ||
| history.replace('/projects') | ||
| } | ||
| } | ||
| }) | ||
|
|
||
| return () => { | ||
| isActive = false | ||
| } | ||
| }, [history, loadOnlyProjectInfo, projectId]) | ||
| }, [history, loadOnlyProjectInfo, projectId, clearProjectDetail]) | ||
|
|
||
| useEffect(() => { | ||
| if ( | ||
|
|
@@ -67,6 +83,15 @@ const ProjectEntry = ({ | |
| history.replace(destination) | ||
| }, [history, isProjectLoading, projectDetail, resolvedProjectId, token]) | ||
|
|
||
| if (projectAccessDenied) { | ||
| return ( | ||
| <Challenges | ||
| menu='NULL' | ||
| warnMessage={PROJECT_ACCESS_DENIED_MESSAGE} | ||
| /> | ||
| ) | ||
| } | ||
|
|
||
| return <Loader /> | ||
| } | ||
|
|
||
|
|
@@ -76,6 +101,7 @@ ProjectEntry.propTypes = { | |
| }).isRequired, | ||
| isProjectLoading: PropTypes.bool, | ||
| loadOnlyProjectInfo: PropTypes.func.isRequired, | ||
| clearProjectDetail: PropTypes.func.isRequired, | ||
| match: PropTypes.shape({ | ||
| params: PropTypes.shape({ | ||
| projectId: PropTypes.string | ||
|
|
@@ -92,7 +118,8 @@ const mapStateToProps = ({ auth, projects }) => ({ | |
| }) | ||
|
|
||
| const mapDispatchToProps = { | ||
| loadOnlyProjectInfo | ||
| loadOnlyProjectInfo, | ||
| clearProjectDetail | ||
| } | ||
|
|
||
| export default withRouter( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[⚠️
correctness]The use of
warnMessage.includes('{linkComponent}')andwarnMessage.replace('{linkComponent}', '')could lead to unexpected behavior ifwarnMessagecontains multiple instances of{linkComponent}. Consider using a more robust method to ensure only the intended placeholder is replaced.