--- /dev/null
+---
+title: Organization and Project Privacy
+scope: sonarcloud
+---
+
+## TODO
*/
import { getJSON, post, postJSON, RequestData } from '../helpers/request';
import throwGlobalError from '../app/utils/throwGlobalError';
-import { Paging } from '../app/types';
+import { LightOrganization, Paging } from '../app/types';
-interface GetOrganizationsParameters {
+export function getOrganizations(data: {
organizations?: string;
member?: boolean;
-}
-
-interface GetOrganizationsResponse {
- organizations: Array<{
- avatar?: string;
- description?: string;
- guarded: boolean;
- isAdmin: boolean;
- key: string;
- name: string;
- url?: string;
- }>;
- paging: {
- pageIndex: number;
- pageSize: number;
- total: number;
- };
-}
-
-export function getOrganizations(
- data: GetOrganizationsParameters
-): Promise<GetOrganizationsResponse> {
+}): Promise<{
+ organizations: LightOrganization[];
+ paging: Paging;
+}> {
return getJSON('/api/organizations/search', data);
}
import ExtensionNotFound from './ExtensionNotFound';
import { getOrganizationByKey } from '../../../store/rootReducer';
import { fetchOrganization } from '../../../apps/organizations/actions';
-/*:: import type { Organization } from '../../../store/organizations/duck'; */
+/*:: import type { Organization } from '../../../app/types'; */
/*::
type Props = {
}
const mapStateToProps = (state: any, ownProps: OwnProps): StateProps => ({
- organization:
- ownProps.component.organization && getOrganizationByKey(state, ownProps.component.organization),
+ organization: getOrganizationByKey(state, ownProps.component.organization),
shouldOrganizationBeDisplayed: areThereCustomOrganizations(state)
});
projectName?: string;
type: string;
}
+export interface LightOrganization {
+ avatar?: string;
+ description?: string;
+ guarded?: boolean;
+ isAdmin?: boolean;
+ key: string;
+ name: string;
+ subscription?: OrganizationSubscription;
+ url?: string;
+}
-export interface Organization {
+export interface Organization extends LightOrganization {
adminPages?: { key: string; name: string }[];
- avatar?: string;
canAdmin?: boolean;
canDelete?: boolean;
canProvisionProjects?: boolean;
canUpdateProjectsVisibilityToPrivate?: boolean;
- description?: string;
- isAdmin?: boolean;
isDefault?: boolean;
- key: string;
- name: string;
pages?: { key: string; name: string }[];
- projectVisibility: Visibility;
- url?: string;
+ projectVisibility?: Visibility;
+}
+
+export enum OrganizationSubscription {
+ Free = 'FREE',
+ Paid = 'PAID',
+ SonarQube = 'SONARQUBE'
}
export interface Paging {
interface Props {
canWrite: boolean | undefined;
+ hidePermalink?: boolean;
hideSimilarRulesFilter?: boolean;
onFilterChange: (changes: Partial<Query>) => void;
onTagsChange: (tags: string[]) => void;
};
render() {
- const { ruleDetails } = this.props;
+ const { hidePermalink, ruleDetails } = this.props;
const hasTypeData = !ruleDetails.isExternal || ruleDetails.type !== 'UNKNOWN';
return (
<div className="js-rule-meta">
<header className="page-header">
<div className="pull-right">
<span className="note text-middle">{ruleDetails.key}</span>
- {!ruleDetails.isExternal && (
- <Link
- className="coding-rules-detail-permalink link-no-underline spacer-left text-middle"
- title={translate('permalink')}
- to={getRuleUrl(ruleDetails.key, this.props.organization)}>
- <LinkIcon />
- </Link>
- )}
+ {!ruleDetails.isExternal &&
+ !hidePermalink && (
+ <Link
+ className="coding-rules-detail-permalink link-no-underline spacer-left text-middle"
+ title={translate('permalink')}
+ to={getRuleUrl(ruleDetails.key, this.props.organization)}>
+ <LinkIcon />
+ </Link>
+ )}
{!this.props.hideSimilarRulesFilter && (
<SimilarRulesFilter onFilterChange={this.props.onFilterChange} rule={ruleDetails} />
)}
expect(onTagsChange).toBeCalledWith(['foo', 'bar']);
});
+it('should not display rule permalink', () => {
+ expect(
+ getWrapper({ hidePermalink: true })
+ .find('.coding-rules-detail-permalink')
+ .exists()
+ ).toBeFalsy();
+});
+
function getWrapper(props = {}) {
return shallow(
<RuleDetailsMeta
import { getOrganizationMembersState } from '../../store/rootReducer';
import { addGlobalSuccessMessage } from '../../store/globalMessages/duck';
import { translate, translateWithParameters } from '../../helpers/l10n';
-/*:: import type { Organization } from '../../store/organizations/duck'; */
+/*:: import type { Organization } from '../../app/types'; */
/*:: import type { Member } from '../../store/organizationsMembers/actions'; */
const PAGE_SIZE = 50;
import React from 'react';
import MembersListItem from './MembersListItem';
/*:: import type { Member } from '../../../store/organizationsMembers/actions'; */
-/*:: import type { Organization, OrgGroup } from '../../../store/organizations/duck'; */
+/*:: import type { Organization, Group } from '../../../app/types'; */
/*::
type Props = {
members: Array<Member>,
- organizationGroups: Array<OrgGroup>,
+ organizationGroups: Array<Group>,
organization: Organization,
removeMember: Member => void,
updateMemberGroups: (member: Member, add: Array<string>, remove: Array<string>) => void
ActionsDropdownItem
} from '../../../components/controls/ActionsDropdown';
/*:: import type { Member } from '../../../store/organizationsMembers/actions'; */
-/*:: import type { Organization, OrgGroup } from '../../../store/organizations/duck'; */
+/*:: import type { Organization, Group } from '../../../app/types'; */
/*::
type Props = {
member: Member,
organization: Organization,
- organizationGroups: Array<OrgGroup>,
+ organizationGroups: Array<Group>,
removeMember: Member => void,
updateMemberGroups: (member: Member, add: Array<string>, remove: Array<string>) => void
};
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { connect } from 'react-redux';
+import { RouterState } from 'react-router';
+import { getOrganizationByKey, getCurrentUser } from '../../../store/rootReducer';
+import handleRequiredAuthorization from '../../../app/utils/handleRequiredAuthorization';
+import { Organization, CurrentUser, isLoggedIn } from '../../../app/types';
+import { isCurrentUserMemberOf, hasPrivateAccess } from '../../../helpers/organizations';
+
+interface StateToProps {
+ currentUser: CurrentUser;
+ organization?: Organization;
+}
+
+interface OwnProps extends RouterState {
+ children: JSX.Element;
+}
+
+interface Props extends StateToProps, Pick<OwnProps, 'children' | 'location'> {
+ hasAccess: (props: Props) => boolean;
+}
+
+export class OrganizationAccess extends React.PureComponent<Props> {
+ componentDidMount() {
+ this.checkPermissions();
+ }
+
+ componentDidUpdate() {
+ this.checkPermissions();
+ }
+
+ checkPermissions = () => {
+ if (!this.props.hasAccess(this.props)) {
+ handleRequiredAuthorization();
+ }
+ };
+
+ render() {
+ if (!this.props.hasAccess(this.props)) {
+ return null;
+ }
+ return React.cloneElement(this.props.children, {
+ location: this.props.location,
+ organization: this.props.organization
+ });
+ }
+}
+
+const mapStateToProps = (state: any, ownProps: OwnProps) => ({
+ currentUser: getCurrentUser(state),
+ organization: getOrganizationByKey(state, ownProps.params.organizationKey)
+});
+
+const OrganizationAccessContainer = connect<StateToProps, {}, OwnProps>(mapStateToProps)(
+ OrganizationAccess
+);
+
+export function OrganizationPrivateAccess(props: OwnProps) {
+ return (
+ <OrganizationAccessContainer
+ hasAccess={({ organization }: StateToProps) => hasPrivateAccess(organization)}
+ {...props}
+ />
+ );
+}
+
+export function OrganizationMembersAccess(props: OwnProps) {
+ return (
+ <OrganizationAccessContainer
+ hasAccess={({ organization }: StateToProps) => isCurrentUserMemberOf(organization)}
+ {...props}
+ />
+ );
+}
+
+export function hasAdminAccess({
+ currentUser,
+ organization
+}: Pick<StateToProps, 'currentUser' | 'organization'>) {
+ const isAdmin = isLoggedIn(currentUser) && organization && organization.canAdmin;
+ return Boolean(isAdmin);
+}
+
+export function OrganizationAdminAccess(props: OwnProps) {
+ return <OrganizationAccessContainer hasAccess={hasAdminAccess} {...props} />;
+}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import * as React from 'react';
-import { connect } from 'react-redux';
-import { RouterState } from 'react-router';
-import { getOrganizationByKey } from '../../../store/rootReducer';
-import handleRequiredAuthorization from '../../../app/utils/handleRequiredAuthorization';
-import { Organization } from '../../../app/types';
-
-interface StateToProps {
- organization?: Organization;
-}
-
-interface OwnProps extends RouterState {
- children: JSX.Element;
-}
-
-interface Props extends StateToProps, Pick<OwnProps, 'children' | 'location'> {}
-
-export class OrganizationAdmin extends React.PureComponent<Props> {
- componentDidMount() {
- this.checkPermissions();
- }
-
- componentDidUpdate() {
- this.checkPermissions();
- }
-
- isOrganizationAdmin = () => this.props.organization && this.props.organization.canAdmin;
-
- checkPermissions = () => {
- if (!this.isOrganizationAdmin()) {
- handleRequiredAuthorization();
- }
- };
-
- render() {
- if (!this.isOrganizationAdmin()) {
- return null;
- }
- return React.cloneElement(this.props.children, {
- location: this.props.location,
- organization: this.props.organization
- });
- }
-}
-
-const mapStateToProps = (state: any, ownProps: OwnProps) => ({
- organization: getOrganizationByKey(state, ownProps.params.organizationKey)
-});
-
-export default connect<StateToProps, {}, OwnProps>(mapStateToProps)(OrganizationAdmin);
import { connect } from 'react-redux';
import { debounce } from 'lodash';
import { translate } from '../../../helpers/l10n';
-/*:: import type { Organization } from '../../../store/organizations/duck'; */
-import { getOrganizationByKey } from '../../../store/rootReducer';
import { updateOrganization } from '../actions';
import { SubmitButton } from '../../../components/ui/buttons';
+/*:: import type { Organization } from '../../../app/types'; */
/*::
type Props = {
//@flow
import React from 'react';
import Checkbox from '../../../components/controls/Checkbox';
-/*:: import type { OrgGroup } from '../../../store/organizations/duck'; */
+/*:: import type { Group } from '../../../app/types'; */
/*::
type Props = {
- group: OrgGroup,
+ group: Group,
checked: boolean,
onCheck: (string, boolean) => void
};
import ListFooter from '../../../components/controls/ListFooter';
import DocTooltip from '../../../components/docs/DocTooltip';
import { translate } from '../../../helpers/l10n';
-/*:: import type { Organization, OrgGroup } from '../../../store/organizations/duck'; */
+/*:: import type { Organization, Group } from '../../../app/types'; */
/*:: import type { Member } from '../../../store/organizationsMembers/actions'; */
/*::
type Props = {
members: Array<Member>,
memberLogins: Array<string>,
- organizationGroups: Array<OrgGroup>,
+ organizationGroups: Array<Group>,
status: { loading?: boolean, total?: number, pageIndex?: number, query?: string },
organization: Organization,
fetchOrganizationMembers: (organizationKey: string, query?: string) => void,
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-// @flow
-import React from 'react';
-import Helmet from 'react-helmet';
-import { connect } from 'react-redux';
-import OrganizationNavigation from '../navigation/OrganizationNavigation';
-import NotFound from '../../../app/components/NotFound';
-import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
-import { fetchOrganization } from '../actions';
-import { getOrganizationByKey } from '../../../store/rootReducer';
-/*:: import type { Organization } from '../../../store/organizations/duck'; */
-
-/*::
-type OwnProps = {
- params: { organizationKey: string }
-};
-*/
-
-/*::
-type Props = {
- children?: React.Element<*>,
- location: Object,
- organization: null | Organization,
- params: { organizationKey: string },
- fetchOrganization: string => Promise<*>
-};
-*/
-
-/*::
-type State = {
- loading: boolean
-};
-*/
-
-export class OrganizationPage extends React.PureComponent {
- /*:: mounted: boolean; */
- /*:: props: Props; */
- state /*: State */ = { loading: true };
-
- componentDidMount() {
- this.mounted = true;
- this.updateOrganization(this.props.params.organizationKey);
- }
-
- componentWillReceiveProps(nextProps /*: Props */) {
- if (nextProps.params.organizationKey !== this.props.params.organizationKey) {
- this.updateOrganization(nextProps.params.organizationKey);
- }
- }
-
- componentWillUnmount() {
- this.mounted = false;
- }
-
- stopLoading = () => {
- if (this.mounted) {
- this.setState({ loading: false });
- }
- };
-
- updateOrganization = (organizationKey /*: string */) => {
- this.setState({ loading: true });
- this.props.fetchOrganization(organizationKey).then(this.stopLoading, this.stopLoading);
- };
-
- render() {
- const { organization } = this.props;
-
- if (!organization || organization.canAdmin == null) {
- if (this.state.loading) {
- return null;
- } else {
- return <NotFound />;
- }
- }
-
- return (
- <div>
- <Helmet defaultTitle={organization.name} titleTemplate={'%s - ' + organization.name} />
- <Suggestions suggestions="organization_space" />
- <OrganizationNavigation location={this.props.location} organization={organization} />
- {this.props.children}
- </div>
- );
- }
-}
-
-const mapStateToProps = (state, ownProps /*: OwnProps */) => ({
- organization: getOrganizationByKey(state, ownProps.params.organizationKey)
-});
-
-const mapDispatchToProps = { fetchOrganization };
-
-export default connect(mapStateToProps, mapDispatchToProps)(OrganizationPage);
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import Helmet from 'react-helmet';
+import { connect } from 'react-redux';
+import OrganizationNavigation from '../navigation/OrganizationNavigation';
+import NotFound from '../../../app/components/NotFound';
+import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
+import { fetchOrganization } from '../actions';
+import { getOrganizationByKey } from '../../../store/rootReducer';
+import { Organization } from '../../../app/types';
+
+interface OwnProps {
+ children?: React.ReactNode;
+ location: { pathname: string };
+ params: { organizationKey: string };
+}
+
+interface StateProps {
+ organization?: Organization;
+}
+
+interface DispatchToProps {
+ fetchOrganization: (organizationKey: string) => Promise<void>;
+}
+
+type Props = OwnProps & StateProps & DispatchToProps;
+
+interface State {
+ loading: boolean;
+}
+
+export class OrganizationPage extends React.PureComponent<Props, State> {
+ mounted = false;
+ state: State = { loading: true };
+
+ componentDidMount() {
+ this.mounted = true;
+ this.updateOrganization(this.props.params.organizationKey);
+ }
+
+ componentWillReceiveProps(nextProps: Props) {
+ if (nextProps.params.organizationKey !== this.props.params.organizationKey) {
+ this.updateOrganization(nextProps.params.organizationKey);
+ }
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ stopLoading = () => {
+ if (this.mounted) {
+ this.setState({ loading: false });
+ }
+ };
+
+ updateOrganization = (organizationKey: string) => {
+ this.setState({ loading: true });
+ this.props.fetchOrganization(organizationKey).then(this.stopLoading, this.stopLoading);
+ };
+
+ render() {
+ const { organization } = this.props;
+
+ if (!organization || organization.canAdmin == null) {
+ if (this.state.loading) {
+ return null;
+ } else {
+ return <NotFound />;
+ }
+ }
+
+ return (
+ <div>
+ <Helmet defaultTitle={organization.name} titleTemplate={'%s - ' + organization.name} />
+ <Suggestions suggestions="organization_space" />
+ <OrganizationNavigation location={this.props.location} organization={organization} />
+ {this.props.children}
+ </div>
+ );
+ }
+}
+
+const mapStateToProps = (state: any, ownProps: OwnProps) => ({
+ organization: getOrganizationByKey(state, ownProps.params.organizationKey)
+});
+
+const mapDispatchToProps = { fetchOrganization: fetchOrganization as any };
+
+export default connect<StateProps, DispatchToProps, OwnProps>(mapStateToProps, mapDispatchToProps)(
+ OrganizationPage
+);
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import { Location } from 'history';
+import { hasAdminAccess, OrganizationAccess } from '../OrganizationAccessContainer';
+import { Visibility } from '../../../../app/types';
+
+jest.mock('../../../../app/utils/handleRequiredAuthorization', () => ({ default: jest.fn() }));
+
+const locationMock = {} as Location;
+
+const currentUser = {
+ isLoggedIn: false
+};
+
+const loggedInUser = {
+ isLoggedIn: true,
+ login: 'luke',
+ name: 'Skywalker',
+ showOnboardingTutorial: false
+};
+
+const organization = {
+ canAdmin: false,
+ key: 'foo',
+ name: 'Foo',
+ projectVisibility: Visibility.Public
+};
+
+const adminOrganization = { ...organization, canAdmin: true };
+
+describe('component', () => {
+ it('should render children', () => {
+ expect(
+ shallow(
+ <OrganizationAccess
+ currentUser={loggedInUser}
+ hasAccess={() => true}
+ location={locationMock}
+ organization={adminOrganization}>
+ <div>hello</div>
+ </OrganizationAccess>
+ )
+ ).toMatchSnapshot();
+ });
+
+ it('should not render anything', () => {
+ expect(
+ shallow(
+ <OrganizationAccess
+ currentUser={loggedInUser}
+ hasAccess={() => false}
+ location={locationMock}
+ organization={adminOrganization}>
+ <div>hello</div>
+ </OrganizationAccess>
+ ).type()
+ ).toBeNull();
+ });
+});
+
+describe('access functions', () => {
+ it('should correctly handle access to admin only space', () => {
+ expect(
+ hasAdminAccess({ currentUser: loggedInUser, organization: adminOrganization })
+ ).toBeTruthy();
+ expect(hasAdminAccess({ currentUser, organization: adminOrganization })).toBeFalsy();
+ expect(hasAdminAccess({ currentUser: loggedInUser, organization })).toBeFalsy();
+ });
+});
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import * as React from 'react';
-import { shallow } from 'enzyme';
-import { Location } from 'history';
-import { OrganizationAdmin } from '../OrganizationAdminContainer';
-import { Visibility } from '../../../../app/types';
-
-jest.mock('../../../../app/utils/handleRequiredAuthorization', () => ({ default: jest.fn() }));
-
-const locationMock = {} as Location;
-
-it('should render children', () => {
- const organization = {
- canAdmin: true,
- key: 'foo',
- name: 'Foo',
- projectVisibility: Visibility.Public
- };
- expect(
- shallow(
- <OrganizationAdmin organization={organization} location={locationMock}>
- <div>hello</div>
- </OrganizationAdmin>
- )
- ).toMatchSnapshot();
-});
-
-it('should not render anything', () => {
- const organization = {
- canAdmin: false,
- key: 'foo',
- name: 'Foo',
- projectVisibility: Visibility.Public
- };
- expect(
- shallow(
- <OrganizationAdmin organization={organization} location={locationMock}>
- <div>hello</div>
- </OrganizationAdmin>
- ).type()
- ).toBeNull();
-});
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`component should render children 1`] = `
+<div
+ location={Object {}}
+ organization={
+ Object {
+ "canAdmin": true,
+ "key": "foo",
+ "name": "Foo",
+ "projectVisibility": "public",
+ }
+ }
+>
+ hello
+</div>
+`;
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render children 1`] = `
-<div
- location={Object {}}
- organization={
- Object {
- "canAdmin": true,
- "key": "foo",
- "name": "Foo",
- "projectVisibility": "public",
- }
- }
->
- hello
-</div>
-`;
import Modal from '../../../../components/controls/Modal';
import { translate } from '../../../../helpers/l10n';
import { SubmitButton, ResetButtonLink, Button } from '../../../../components/ui/buttons';
-/*:: import type { Organization } from '../../../../store/organizations/duck'; */
+/*:: import type { Organization } from '../../../../app/types'; */
/*:: import type { Member } from '../../../../store/organizationsMembers/actions'; */
/*::
import OrganizationGroupCheckbox from '../OrganizationGroupCheckbox';
import { SubmitButton, ResetButtonLink } from '../../../../components/ui/buttons';
/*:: import type { Member } from '../../../../store/organizationsMembers/actions'; */
-/*:: import type { Organization, OrgGroup } from '../../../../store/organizations/duck'; */
+/*:: import type { Organization, Group } from '../../../../app/types'; */
/*::
type Props = {
onClose: () => void;
member: Member,
organization: Organization,
- organizationGroups: Array<OrgGroup>,
+ organizationGroups: Array<Group>,
updateMemberGroups: (member: Member, add: Array<string>, remove: Array<string>) => void
};
*/
import { translate, translateWithParameters } from '../../../../helpers/l10n';
import { SubmitButton, ResetButtonLink } from '../../../../components/ui/buttons';
/*:: import type { Member } from '../../../../store/organizationsMembers/actions'; */
-/*:: import type { Organization } from '../../../../store/organizations/duck'; */
+/*:: import type { Organization } from '../../../../app/types'; */
/*::
type Props = {
organization: Organization;
}
-export default function OrganizationNavigation({ organization, location }: Props) {
+export default function OrganizationNavigation({ location, organization }: Props) {
return (
<ContextNavBar height={theme.contextNavHeightRaw} id="context-navigation">
<div className="navbar-context-justified">
import NavBarTabs from '../../../components/nav/NavBarTabs';
import { translate } from '../../../helpers/l10n';
import { getQualityGatesUrl } from '../../../helpers/urls';
+import { hasPrivateAccess, isCurrentUserMemberOf } from '../../../helpers/organizations';
interface Props {
location: { pathname: string };
{translate('issues.page')}
</Link>
</li>
- <li>
- <Link activeClassName="active" to={`/organizations/${organization.key}/quality_profiles`}>
- {translate('quality_profiles.page')}
- </Link>
- </li>
- <li>
- <Link activeClassName="active" to={`/organizations/${organization.key}/rules`}>
- {translate('coding_rules.page')}
- </Link>
- </li>
- <li>
- <Link activeClassName="active" to={getQualityGatesUrl(organization.key)}>
- {translate('quality_gates.page')}
- </Link>
- </li>
- <li>
- <Link activeClassName="active" to={`/organizations/${organization.key}/members`}>
- {translate('organization.members.page')}
- </Link>
- </li>
+ {hasPrivateAccess(organization) && (
+ <>
+ <li>
+ <Link
+ activeClassName="active"
+ to={`/organizations/${organization.key}/quality_profiles`}>
+ {translate('quality_profiles.page')}
+ </Link>
+ </li>
+ <li>
+ <Link activeClassName="active" to={`/organizations/${organization.key}/rules`}>
+ {translate('coding_rules.page')}
+ </Link>
+ </li>
+ <li>
+ <Link activeClassName="active" to={getQualityGatesUrl(organization.key)}>
+ {translate('quality_gates.page')}
+ </Link>
+ </li>
+ </>
+ )}
+
+ {isCurrentUserMemberOf(organization) && (
+ <li>
+ <Link activeClassName="active" to={`/organizations/${organization.key}/members`}>
+ {translate('organization.members.page')}
+ </Link>
+ </li>
+ )}
+
<OrganizationNavigationExtensions location={location} organization={organization} />
{organization.canAdmin && (
<OrganizationNavigationAdministration location={location} organization={organization} />
import { shallow } from 'enzyme';
import OrganizationNavigationMenu from '../OrganizationNavigationMenu';
import { Visibility } from '../../../../app/types';
+import { isCurrentUserMemberOf, hasPrivateAccess } from '../../../../helpers/organizations';
+
+jest.mock('../../../../helpers/organizations', () => ({
+ isCurrentUserMemberOf: jest.fn().mockReturnValue(true),
+ hasPrivateAccess: jest.fn().mockReturnValue(true)
+}));
+
+const organization = {
+ key: 'foo',
+ name: 'Foo',
+ projectVisibility: Visibility.Public
+};
+
+beforeEach(() => {
+ (isCurrentUserMemberOf as jest.Mock<any>).mockClear();
+ (hasPrivateAccess as jest.Mock<any>).mockClear();
+});
it('renders', () => {
expect(
- shallow(
- <OrganizationNavigationMenu
- location={{ pathname: '' }}
- organization={{
- key: 'foo',
- name: 'Foo',
- projectVisibility: Visibility.Public
- }}
- />
- )
+ shallow(<OrganizationNavigationMenu location={{ pathname: '' }} organization={organization} />)
).toMatchSnapshot();
});
shallow(
<OrganizationNavigationMenu
location={{ pathname: '' }}
- organization={{
- canAdmin: true,
- key: 'foo',
- name: 'Foo',
- projectVisibility: Visibility.Public
- }}
+ organization={{ ...organization, canAdmin: true }}
/>
)
).toMatchSnapshot();
issues.page
</Link>
</li>
- <li>
- <Link
- activeClassName="active"
- onlyActiveOnIndex={false}
- style={Object {}}
- to="/organizations/foo/quality_profiles"
- >
- quality_profiles.page
- </Link>
- </li>
- <li>
- <Link
- activeClassName="active"
- onlyActiveOnIndex={false}
- style={Object {}}
- to="/organizations/foo/rules"
- >
- coding_rules.page
- </Link>
- </li>
- <li>
- <Link
- activeClassName="active"
- onlyActiveOnIndex={false}
- style={Object {}}
- to={
- Object {
- "pathname": "/organizations/foo/quality_gates",
+ <React.Fragment>
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to="/organizations/foo/quality_profiles"
+ >
+ quality_profiles.page
+ </Link>
+ </li>
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to="/organizations/foo/rules"
+ >
+ coding_rules.page
+ </Link>
+ </li>
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/organizations/foo/quality_gates",
+ }
}
- }
- >
- quality_gates.page
- </Link>
- </li>
+ >
+ quality_gates.page
+ </Link>
+ </li>
+ </React.Fragment>
<li>
<Link
activeClassName="active"
issues.page
</Link>
</li>
- <li>
- <Link
- activeClassName="active"
- onlyActiveOnIndex={false}
- style={Object {}}
- to="/organizations/foo/quality_profiles"
- >
- quality_profiles.page
- </Link>
- </li>
- <li>
- <Link
- activeClassName="active"
- onlyActiveOnIndex={false}
- style={Object {}}
- to="/organizations/foo/rules"
- >
- coding_rules.page
- </Link>
- </li>
- <li>
- <Link
- activeClassName="active"
- onlyActiveOnIndex={false}
- style={Object {}}
- to={
- Object {
- "pathname": "/organizations/foo/quality_gates",
+ <React.Fragment>
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to="/organizations/foo/quality_profiles"
+ >
+ quality_profiles.page
+ </Link>
+ </li>
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to="/organizations/foo/rules"
+ >
+ coding_rules.page
+ </Link>
+ </li>
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/organizations/foo/quality_gates",
+ }
}
- }
- >
- quality_gates.page
- </Link>
- </li>
+ >
+ quality_gates.page
+ </Link>
+ </li>
+ </React.Fragment>
<li>
<Link
activeClassName="active"
]
},
{
- path: 'members',
- component: lazyLoad(() => import('./components/OrganizationMembersContainer'))
- },
- {
- path: 'rules',
- component: OrganizationContainer,
- childRoutes: codingRulesRoutes
- },
- {
- path: 'quality_profiles',
- childRoutes: qualityProfilesRoutes
+ component: lazyLoad(() =>
+ import('./components/OrganizationAccessContainer').then(lib => ({
+ default: lib.OrganizationMembersAccess
+ }))
+ ),
+ childRoutes: [
+ {
+ path: 'members',
+ component: lazyLoad(() => import('./components/OrganizationMembersContainer'))
+ }
+ ]
},
{
- path: 'quality_gates',
- component: OrganizationContainer,
- childRoutes: qualityGatesRoutes
+ component: lazyLoad(() =>
+ import('./components/OrganizationAccessContainer').then(lib => ({
+ default: lib.OrganizationPrivateAccess
+ }))
+ ),
+ childRoutes: [
+ {
+ path: 'rules',
+ component: OrganizationContainer,
+ childRoutes: codingRulesRoutes
+ },
+ {
+ path: 'quality_profiles',
+ childRoutes: qualityProfilesRoutes
+ },
+ {
+ path: 'quality_gates',
+ component: OrganizationContainer,
+ childRoutes: qualityGatesRoutes
+ }
+ ]
},
{
- component: lazyLoad(() => import('./components/OrganizationAdminContainer')),
+ component: lazyLoad(() =>
+ import('./components/OrganizationAccessContainer').then(lib => ({
+ default: lib.OrganizationAdminAccess
+ }))
+ ),
childRoutes: [
{ path: 'delete', component: lazyLoad(() => import('./components/OrganizationDelete')) },
{ path: 'edit', component: lazyLoad(() => import('./components/OrganizationEdit')) },
import { History } from '../../../api/time-machine';
import { translate } from '../../../helpers/l10n';
import { MeasureEnhanced } from '../../../helpers/measures';
+import { hasPrivateAccess } from '../../../helpers/organizations';
interface Props {
branchLike?: BranchLike;
organizationsEnabled: PropTypes.bool
};
+ renderQualityInfos() {
+ const { organizationsEnabled } = this.context;
+ const { organization, qualifier, qualityProfiles, qualityGate } = this.props.component;
+ const isProject = qualifier === 'TRK';
+
+ if (!isProject || (organizationsEnabled && !hasPrivateAccess(organization))) {
+ return null;
+ }
+
+ return (
+ <div className="overview-meta-card">
+ {qualityGate && (
+ <MetaQualityGate
+ organization={organizationsEnabled ? organization : undefined}
+ qualityGate={qualityGate}
+ />
+ )}
+
+ {qualityProfiles &&
+ qualityProfiles.length > 0 && (
+ <MetaQualityProfiles
+ headerClassName={qualityGate ? 'big-spacer-top' : undefined}
+ organization={organizationsEnabled ? organization : undefined}
+ profiles={qualityProfiles}
+ />
+ )}
+ </div>
+ );
+ }
+
render() {
const { organizationsEnabled } = this.context;
const { branchLike, component, metrics } = this.props;
- const { qualifier, description, qualityProfiles, qualityGate, visibility } = component;
+ const { qualifier, description, visibility } = component;
const isProject = qualifier === 'TRK';
const isApp = qualifier === 'APP';
qualifier={component.qualifier}
/>
- {isProject && (
- <div className="overview-meta-card">
- {qualityGate && (
- <MetaQualityGate
- organization={organizationsEnabled ? component.organization : undefined}
- qualityGate={qualityGate}
- />
- )}
-
- {qualityProfiles &&
- qualityProfiles.length > 0 && (
- <MetaQualityProfiles
- headerClassName={qualityGate ? 'big-spacer-top' : undefined}
- organization={organizationsEnabled ? component.organization : undefined}
- profiles={qualityProfiles}
- />
- )}
- </div>
- )}
+ {this.renderQualityInfos()}
{isProject && <MetaLinks component={component} />}
import { connect } from 'react-redux';
import App from './App';
import forSingleOrganization from '../organizations/forSingleOrganization';
-import { Organization, LoggedInUser } from '../../app/types';
+import { Organization, LoggedInUser, Visibility } from '../../app/types';
import { getAppState, getOrganizationByKey, getCurrentUser } from '../../store/rootReducer';
import { receiveOrganizations } from '../../store/organizations/duck';
import { changeProjectDefaultVisibility } from '../../api/permissions';
ownProps.organization || getOrganizationByKey(state, getAppState(state).defaultOrganization)
});
-const onVisibilityChange = (organization: Organization, visibility: string) => (
+const onVisibilityChange = (organization: Organization, visibility: Visibility) => (
dispatch: Function
) => {
const currentVisibility = organization.projectVisibility;
const mapDispatchToProps = (dispatch: Function) => ({
fetchOrganization: (key: string) => dispatch(fetchOrganization(key)),
- onVisibilityChange: (organization: Organization, visibility: string) =>
+ onVisibilityChange: (organization: Organization, visibility: Visibility) =>
dispatch(onVisibilityChange(organization, visibility))
});
import { Link } from 'react-router';
import { FormattedMessage } from 'react-intl';
import { createProject } from '../../api/components';
-import { Organization } from '../../app/types';
+import { Organization, Visibility } from '../../app/types';
import UpgradeOrganizationBox from '../../components/common/UpgradeOrganizationBox';
import VisibilitySelector from '../../components/common/VisibilitySelector';
import Modal from '../../components/controls/Modal';
key: string;
loading: boolean;
name: string;
- visibility: string;
+ visibility?: Visibility;
// add index declaration to be able to do `this.setState({ [name]: value });`
[x: string]: any;
}
this.setState({ [name]: value });
};
- handleVisibilityChange = (visibility: string) => {
+ handleVisibilityChange = (visibility: Visibility) => {
this.setState({ visibility });
};
<h1 className="page-title">{translate('projects_management')}</h1>
<div className="page-actions">
- {!isSonarCloud() && (
- <span className="big-spacer-right">
- <span className="text-middle">
- {translate('organization.default_visibility_of_new_projects')}{' '}
- <strong>{translate('visibility', organization.projectVisibility)}</strong>
+ {!isSonarCloud() &&
+ organization.projectVisibility && (
+ <span className="big-spacer-right">
+ <span className="text-middle">
+ {translate('organization.default_visibility_of_new_projects')}{' '}
+ <strong>{translate('visibility', organization.projectVisibility)}</strong>
+ </span>
+ <EditButton
+ className="js-change-visibility spacer-left button-small"
+ onClick={this.handleChangeVisibilityClick}
+ />
</span>
- <EditButton
- className="js-change-visibility spacer-left button-small"
- onClick={this.handleChangeVisibilityClick}
- />
- </span>
- )}
+ )}
{this.props.hasProvisionPermission && (
<Button id="create-project" onClick={this.props.onProjectCreate}>
{translate('qualifiers.create.TRK')}
import * as React from 'react';
import * as classNames from 'classnames';
import { translate } from '../../helpers/l10n';
+import { Visibility } from '../../app/types';
interface Props {
canTurnToPrivate?: boolean;
className?: string;
- onChange: (x: string) => void;
- visibility: string;
+ onChange: (visibility: Visibility) => void;
+ visibility?: Visibility;
}
export default class VisibilitySelector extends React.PureComponent<Props> {
handlePublicClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
event.currentTarget.blur();
- this.props.onChange('public');
+ this.props.onChange(Visibility.Public);
};
handlePrivateClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
event.currentTarget.blur();
- this.props.onChange('private');
+ this.props.onChange(Visibility.Private);
};
render() {
<div className={this.props.className}>
<a
className="link-base-color link-no-underline"
- id="visibility-public"
href="#"
+ id="visibility-public"
onClick={this.handlePublicClick}>
<i
className={classNames('icon-radio', {
- 'is-checked': this.props.visibility === 'public'
+ 'is-checked': this.props.visibility === Visibility.Public
})}
/>
<span className="spacer-left">{translate('visibility.public')}</span>
{this.props.canTurnToPrivate ? (
<a
className="link-base-color link-no-underline huge-spacer-left"
- id="visibility-private"
href="#"
+ id="visibility-private"
onClick={this.handlePrivateClick}>
<i
className={classNames('icon-radio', {
- 'is-checked': this.props.visibility === 'private'
+ 'is-checked': this.props.visibility === Visibility.Private
})}
/>
<span className="spacer-left">{translate('visibility.private')}</span>
<span className="huge-spacer-left text-muted cursor-not-allowed" id="visibility-private">
<i
className={classNames('icon-radio', {
- 'is-checked': this.props.visibility === 'private'
+ 'is-checked': this.props.visibility === Visibility.Private
})}
/>
<span className="spacer-left">{translate('visibility.private')}</span>
import { shallow } from 'enzyme';
import VisibilitySelector from '../VisibilitySelector';
import { click } from '../../../helpers/testUtils';
+import { Visibility } from '../../../app/types';
it('changes visibility', () => {
const onChange = jest.fn();
const wrapper = shallow(
- <VisibilitySelector canTurnToPrivate={true} onChange={onChange} visibility="public" />
+ <VisibilitySelector
+ canTurnToPrivate={true}
+ onChange={onChange}
+ visibility={Visibility.Public}
+ />
);
expect(wrapper).toMatchSnapshot();
click(wrapper.find('#visibility-private'));
- expect(onChange).toBeCalledWith('private');
+ expect(onChange).toBeCalledWith(Visibility.Private);
- wrapper.setProps({ visibility: 'private' });
+ wrapper.setProps({ visibility: Visibility.Private });
expect(wrapper).toMatchSnapshot();
click(wrapper.find('#visibility-public'));
- expect(onChange).toBeCalledWith('public');
+ expect(onChange).toBeCalledWith(Visibility.Public);
});
it('renders disabled', () => {
expect(
shallow(
- <VisibilitySelector canTurnToPrivate={false} onChange={jest.fn()} visibility="public" />
+ <VisibilitySelector
+ canTurnToPrivate={false}
+ onChange={jest.fn()}
+ visibility={Visibility.Public}
+ />
)
).toMatchSnapshot();
});
import RuleDetailsMeta from '../../apps/coding-rules/components/RuleDetailsMeta';
import RuleDetailsDescription from '../../apps/coding-rules/components/RuleDetailsDescription';
import '../../apps/coding-rules/styles.css';
+import { hasPrivateAccess } from '../../helpers/organizations';
interface Props {
onLoad: (details: { name: string }) => void;
- organization: string | undefined;
+ organizationKey: string | undefined;
ruleKey: string;
}
componentDidUpdate(prevProps: Props) {
if (
prevProps.ruleKey !== this.props.ruleKey ||
- prevProps.organization !== this.props.organization
+ prevProps.organizationKey !== this.props.organizationKey
) {
this.fetchRuleDetails();
}
fetchRuleDetails = () => {
this.setState({ loading: true });
Promise.all([
- getRulesApp({ organization: this.props.organization }),
- getRuleDetails({ key: this.props.ruleKey, organization: this.props.organization })
+ getRulesApp({ organization: this.props.organizationKey }),
+ getRuleDetails({ key: this.props.ruleKey, organization: this.props.organizationKey })
]).then(
([{ repositories }, { rule }]) => {
if (this.mounted) {
noOp = () => {};
render() {
+ const { organizationKey } = this.props;
+
return (
<DeferredSpinner loading={this.state.loading}>
{this.state.ruleDetails && (
<>
<RuleDetailsMeta
canWrite={false}
+ hidePermalink={!hasPrivateAccess(organizationKey)}
hideSimilarRulesFilter={true}
onFilterChange={this.noOp}
onTagsChange={this.noOp}
- organization={this.props.organization}
+ organization={organizationKey}
referencedRepositories={this.state.referencedRepositories}
ruleDetails={this.state.ruleDetails}
/>
<RuleDetailsDescription
canWrite={false}
onChange={this.noOp}
- organization={this.props.organization}
+ organization={organizationKey}
ruleDetails={this.state.ruleDetails}
/>
</>
<div className="workspace-viewer-container" style={{ height: this.props.height }}>
<WorkspaceRuleDetails
onLoad={this.handleLoaded}
- organization={rule.organization}
+ organizationKey={rule.organization}
ruleKey={rule.key}
/>
</div>
import { shallow } from 'enzyme';
import WorkspaceRuleDetails from '../WorkspaceRuleDetails';
import { waitAndUpdate } from '../../../helpers/testUtils';
+import { OrganizationSubscription, Visibility } from '../../../app/types';
+import { hasPrivateAccess } from '../../../helpers/organizations';
+
+jest.mock('../../../helpers/organizations', () => ({
+ hasPrivateAccess: jest.fn().mockReturnValue(true)
+}));
jest.mock('../../../api/rules', () => ({
getRulesApp: jest.fn(() =>
getRuleDetails: jest.fn(() => Promise.resolve({ rule: { key: 'foo', name: 'Foo' } }))
}));
+const organization = {
+ key: 'foo',
+ name: 'Foo',
+ projectVisibility: Visibility.Public,
+ subscription: OrganizationSubscription.Paid
+};
+
+beforeEach(() => {
+ (hasPrivateAccess as jest.Mock<any>).mockClear();
+});
+
it('should render', async () => {
const wrapper = shallow(
- <WorkspaceRuleDetails onLoad={jest.fn()} organization="org" ruleKey="foo" />
+ <WorkspaceRuleDetails onLoad={jest.fn()} organizationKey={undefined} ruleKey="foo" />
);
expect(wrapper).toMatchSnapshot();
it('should call back on load', async () => {
const onLoad = jest.fn();
const wrapper = shallow(
- <WorkspaceRuleDetails onLoad={onLoad} organization="org" ruleKey="foo" />
+ <WorkspaceRuleDetails onLoad={onLoad} organizationKey={undefined} ruleKey="foo" />
);
await waitAndUpdate(wrapper);
expect(onLoad).toBeCalledWith({ name: 'Foo' });
});
+
+it('should render without permalink', async () => {
+ (hasPrivateAccess as jest.Mock<any>).mockReturnValueOnce(false);
+ const wrapper = shallow(
+ <WorkspaceRuleDetails onLoad={jest.fn()} organizationKey={organization.key} ruleKey="foo" />
+ );
+
+ await waitAndUpdate(wrapper);
+ expect(wrapper.find('RuleDetailsMeta').prop('hidePermalink')).toBeTruthy();
+});
<React.Fragment>
<RuleDetailsMeta
canWrite={false}
+ hidePermalink={false}
hideSimilarRulesFilter={true}
onFilterChange={[Function]}
onTagsChange={[Function]}
- organization="org"
referencedRepositories={
Object {
"repo": Object {
<RuleDetailsDescription
canWrite={false}
onChange={[Function]}
- organization="org"
ruleDetails={
Object {
"key": "foo",
>
<WorkspaceRuleDetails
onLoad={[Function]}
- organization="org"
+ organizationKey="org"
ruleKey="foo"
/>
</div>
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { hasPrivateAccess, isCurrentUserMemberOf } from '../organizations';
+import { getCurrentUser, getMyOrganizations } from '../../store/rootReducer';
+import { OrganizationSubscription } from '../../app/types';
+
+jest.mock('../../app/utils/getStore', () => ({
+ default: () => ({
+ getState: jest.fn()
+ })
+}));
+
+jest.mock('../../store/rootReducer', () => ({
+ getCurrentUser: jest.fn().mockReturnValue({
+ isLoggedIn: true,
+ login: 'luke',
+ name: 'Skywalker',
+ showOnboardingTutorial: false
+ }),
+ getMyOrganizations: jest.fn().mockReturnValue([])
+}));
+
+const organization = {
+ key: 'foo',
+ name: 'Foo',
+ subscription: OrganizationSubscription.Paid
+};
+
+const loggedOut = { isLoggedIn: false };
+
+beforeEach(() => {
+ (getCurrentUser as jest.Mock<any>).mockClear();
+ (getMyOrganizations as jest.Mock<any>).mockClear();
+});
+
+describe('isCurrentUserMemberOf', () => {
+ it('should be a member', () => {
+ expect(isCurrentUserMemberOf({ key: 'bar', name: 'Bar', canAdmin: true })).toBeTruthy();
+
+ (getMyOrganizations as jest.Mock<any>).mockReturnValueOnce([organization]);
+ expect(isCurrentUserMemberOf(organization)).toBeTruthy();
+ });
+
+ it('should not be a member', () => {
+ expect(isCurrentUserMemberOf(undefined)).toBeFalsy();
+ expect(isCurrentUserMemberOf(organization)).toBeFalsy();
+
+ (getMyOrganizations as jest.Mock<any>).mockReturnValueOnce([{ key: 'bar', name: 'Bar' }]);
+ expect(isCurrentUserMemberOf(organization)).toBeFalsy();
+
+ (getCurrentUser as jest.Mock<any>).mockReturnValueOnce(loggedOut);
+ expect(isCurrentUserMemberOf(organization)).toBeFalsy();
+ });
+});
+
+describe('hasPrivateAccess', () => {
+ it('should have access', () => {
+ expect(hasPrivateAccess({ key: 'bar', name: 'Bar' })).toBeTruthy();
+
+ (getMyOrganizations as jest.Mock<any>).mockReturnValueOnce([organization]);
+ expect(hasPrivateAccess(organization)).toBeTruthy();
+ });
+
+ it('should not have access', () => {
+ expect(hasPrivateAccess(organization)).toBeFalsy();
+ });
+});
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+import getStore from '../app/utils/getStore';
+import { Organization, isLoggedIn, OrganizationSubscription } from '../app/types';
+import { getCurrentUser, getMyOrganizations, getOrganizationByKey } from '../store/rootReducer';
+
+function getRealOrganization(
+ organization?: Organization | string,
+ state?: any
+): Organization | undefined {
+ if (typeof organization === 'string') {
+ state = state || getStore().getState();
+ return getOrganizationByKey(state, organization);
+ }
+
+ return organization;
+}
+
+function isPaidOrganization(organization: Organization | undefined): boolean {
+ return Boolean(organization && organization.subscription === OrganizationSubscription.Paid);
+}
+
+export function hasPrivateAccess(organization: Organization | string | undefined): boolean {
+ const realOrg = getRealOrganization(organization);
+ return !isPaidOrganization(realOrg) || isCurrentUserMemberOf(realOrg);
+}
+
+export function isCurrentUserMemberOf(organization: Organization | string | undefined): boolean {
+ const state = getStore().getState();
+ const currentUser = getCurrentUser(state);
+ const userOrganizations = getMyOrganizations(state);
+ const realOrg = getRealOrganization(organization, state);
+ return Boolean(
+ realOrg &&
+ isLoggedIn(currentUser) &&
+ (realOrg.canAdmin || userOrganizations.some(org => org.key === realOrg.key))
+ );
+}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-// @flow
-import { combineReducers } from 'redux';
-import { omit, uniq, without } from 'lodash';
-
-/*::
-export type Organization = {
- adminPages?: Array<{ key: string, name: string }>,
- avatar?: string,
- canAdmin?: boolean,
- canDelete?: boolean,
- canProvisionProjects?: boolean,
- canUpdateProjectsVisibilityToPrivate?: boolean,
- description?: string,
- isAdmin: bool,
- key: string,
- name: string,
- pages?: Array<{ key: string, name: string }>,
- projectVisibility: string,
- url?: string
-};
-*/
-
-/*::
-export type OrgGroup = {
- id: string,
- default: boolean,
- description: string,
- membersCount: number,
- name: string
-};
-*/
-
-/*::
-type ReceiveOrganizationsAction = {
- type: 'RECEIVE_ORGANIZATIONS',
- organizations: Array<Organization>
-};
-*/
-
-/*::
-type ReceiveMyOrganizationsAction = {
- type: 'RECEIVE_MY_ORGANIZATIONS',
- organizations: Array<Organization>
-};
-*/
-
-/*::
-type ReceiveOrganizationGroups = {
- type: 'RECEIVE_ORGANIZATION_GROUPS',
- key: string,
- groups: Array<OrgGroup>
-};
-*/
-
-/*::
-type CreateOrganizationAction = {
- type: 'CREATE_ORGANIZATION',
- organization: Organization
-};
-*/
-
-/*::
-type UpdateOrganizationAction = {
- type: 'UPDATE_ORGANIZATION',
- key: string,
- changes: {}
-};
-*/
-
-/*::
-type DeleteOrganizationAction = {
- type: 'DELETE_ORGANIZATION',
- key: string
-};
-*/
-
-/*::
-type Action =
- | ReceiveOrganizationsAction
- | ReceiveMyOrganizationsAction
- | ReceiveOrganizationGroups
- | CreateOrganizationAction
- | UpdateOrganizationAction
- | DeleteOrganizationAction; */
-
-/*::
-type ByKeyState = {
- [key: string]: Organization
-};
-*/
-
-/*::
-type GroupsState = {
- [key: string]: Array<OrgGroup>
-};
-*/
-
-/*::
-type MyState = Array<string>;
-*/
-
-/*::
-type State = {
- byKey: ByKeyState,
- my: MyState,
- groups: GroupsState
-};
-*/
-
-export function receiveOrganizations(
- organizations /*: Array<Organization> */
-) /*: ReceiveOrganizationsAction */ {
- return {
- type: 'RECEIVE_ORGANIZATIONS',
- organizations
- };
-}
-
-export function receiveMyOrganizations(
- organizations /*: Array<Organization> */
-) /*: ReceiveMyOrganizationsAction */ {
- return {
- type: 'RECEIVE_MY_ORGANIZATIONS',
- organizations
- };
-}
-
-export function receiveOrganizationGroups(
- key /*: string */,
- groups /*: Array<OrgGroup> */
-) /*: receiveOrganizationGroups */ {
- return {
- type: 'RECEIVE_ORGANIZATION_GROUPS',
- key,
- groups
- };
-}
-
-export function createOrganization(
- organization /*: Organization */
-) /*: CreateOrganizationAction */ {
- return {
- type: 'CREATE_ORGANIZATION',
- organization
- };
-}
-
-export function updateOrganization(
- key /*: string */,
- changes /*: {} */
-) /*: UpdateOrganizationAction */ {
- return {
- type: 'UPDATE_ORGANIZATION',
- key,
- changes
- };
-}
-
-export function deleteOrganization(key /*: string */) /*: DeleteOrganizationAction */ {
- return {
- type: 'DELETE_ORGANIZATION',
- key
- };
-}
-
-function onReceiveOrganizations(
- state /*: ByKeyState */,
- action /*: ReceiveOrganizationsAction | ReceiveMyOrganizationsAction */
-) /*: ByKeyState */ {
- const nextState = { ...state };
- action.organizations.forEach(organization => {
- nextState[organization.key] = { ...state[organization.key], ...organization };
- });
- return nextState;
-}
-
-function byKey(state /*: ByKeyState */ = {}, action /*: Action */) {
- switch (action.type) {
- case 'RECEIVE_ORGANIZATIONS':
- case 'RECEIVE_MY_ORGANIZATIONS':
- return onReceiveOrganizations(state, action);
- case 'CREATE_ORGANIZATION':
- return { ...state, [action.organization.key]: { ...action.organization, isAdmin: true } };
- case 'UPDATE_ORGANIZATION':
- return {
- ...state,
- [action.key]: {
- ...state[action.key],
- key: action.key,
- ...action.changes
- }
- };
- case 'DELETE_ORGANIZATION':
- return omit(state, action.key);
- default:
- return state;
- }
-}
-
-function my(state /*: MyState */ = [], action /*: Action */) {
- switch (action.type) {
- case 'RECEIVE_MY_ORGANIZATIONS':
- return uniq([...state, ...action.organizations.map(o => o.key)]);
- case 'CREATE_ORGANIZATION':
- return uniq([...state, action.organization.key]);
- case 'DELETE_ORGANIZATION':
- return without(state, action.key);
- default:
- return state;
- }
-}
-
-function groups(state /*: GroupsState */ = {}, action /*: Action */) {
- if (action.type === 'RECEIVE_ORGANIZATION_GROUPS') {
- return { ...state, [action.key]: action.groups };
- }
- return state;
-}
-
-export default combineReducers({ byKey, my, groups });
-
-export function getOrganizationByKey(state /*: State */, key /*: string */) /*: Organization */ {
- return state.byKey[key];
-}
-
-export function getOrganizationGroupsByKey(
- state /*: State */,
- key /*: string */
-) /*: Array<OrgGroup> */ {
- return state.groups[key] || [];
-}
-
-export function getMyOrganizations(state /*: State */) /*: Array<Organization> */ {
- return state.my.map(key => getOrganizationByKey(state, key));
-}
-
-export function areThereCustomOrganizations(state /*: State */) /*: boolean */ {
- return Object.keys(state.byKey).length > 1;
-}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { combineReducers } from 'redux';
+import { omit, uniq, without } from 'lodash';
+import { Group, Organization } from '../../app/types';
+
+interface ReceiveOrganizationsAction {
+ type: 'RECEIVE_ORGANIZATIONS';
+ organizations: Organization[];
+}
+
+interface ReceiveMyOrganizationsAction {
+ type: 'RECEIVE_MY_ORGANIZATIONS';
+ organizations: Organization[];
+}
+
+interface ReceiveOrganizationGroups {
+ type: 'RECEIVE_ORGANIZATION_GROUPS';
+ key: string;
+ groups: Group[];
+}
+
+interface CreateOrganizationAction {
+ type: 'CREATE_ORGANIZATION';
+ organization: Organization;
+}
+
+interface UpdateOrganizationAction {
+ type: 'UPDATE_ORGANIZATION';
+ key: string;
+ changes: {};
+}
+
+interface DeleteOrganizationAction {
+ type: 'DELETE_ORGANIZATION';
+ key: string;
+}
+
+type Action =
+ | ReceiveOrganizationsAction
+ | ReceiveMyOrganizationsAction
+ | ReceiveOrganizationGroups
+ | CreateOrganizationAction
+ | UpdateOrganizationAction
+ | DeleteOrganizationAction;
+
+interface ByKeyState {
+ [key: string]: Organization;
+}
+
+interface GroupsState {
+ [key: string]: Group[];
+}
+
+type MyState = string[];
+
+interface State {
+ byKey: ByKeyState;
+ my: MyState;
+ groups: GroupsState;
+}
+
+export function receiveOrganizations(organizations: Organization[]): ReceiveOrganizationsAction {
+ return {
+ type: 'RECEIVE_ORGANIZATIONS',
+ organizations
+ };
+}
+
+export function receiveMyOrganizations(
+ organizations: Organization[]
+): ReceiveMyOrganizationsAction {
+ return {
+ type: 'RECEIVE_MY_ORGANIZATIONS',
+ organizations
+ };
+}
+
+export function receiveOrganizationGroups(key: string, groups: Group[]): ReceiveOrganizationGroups {
+ return {
+ type: 'RECEIVE_ORGANIZATION_GROUPS',
+ key,
+ groups
+ };
+}
+
+export function createOrganization(organization: Organization): CreateOrganizationAction {
+ return {
+ type: 'CREATE_ORGANIZATION',
+ organization
+ };
+}
+
+export function updateOrganization(key: string, changes: {}): UpdateOrganizationAction {
+ return {
+ type: 'UPDATE_ORGANIZATION',
+ key,
+ changes
+ };
+}
+
+export function deleteOrganization(key: string): DeleteOrganizationAction {
+ return {
+ type: 'DELETE_ORGANIZATION',
+ key
+ };
+}
+
+function onReceiveOrganizations(
+ state: ByKeyState,
+ action: ReceiveOrganizationsAction | ReceiveMyOrganizationsAction
+): ByKeyState {
+ const nextState = { ...state };
+ action.organizations.forEach(organization => {
+ nextState[organization.key] = { ...state[organization.key], ...organization };
+ });
+ return nextState;
+}
+
+function byKey(state: ByKeyState = {}, action: Action) {
+ switch (action.type) {
+ case 'RECEIVE_ORGANIZATIONS':
+ case 'RECEIVE_MY_ORGANIZATIONS':
+ return onReceiveOrganizations(state, action);
+ case 'CREATE_ORGANIZATION':
+ return { ...state, [action.organization.key]: { ...action.organization, isAdmin: true } };
+ case 'UPDATE_ORGANIZATION':
+ return {
+ ...state,
+ [action.key]: {
+ ...state[action.key],
+ key: action.key,
+ ...action.changes
+ }
+ };
+ case 'DELETE_ORGANIZATION':
+ return omit(state, action.key);
+ default:
+ return state;
+ }
+}
+
+function my(state: MyState = [], action: Action) {
+ switch (action.type) {
+ case 'RECEIVE_MY_ORGANIZATIONS':
+ return uniq([...state, ...action.organizations.map(o => o.key)]);
+ case 'CREATE_ORGANIZATION':
+ return uniq([...state, action.organization.key]);
+ case 'DELETE_ORGANIZATION':
+ return without(state, action.key);
+ default:
+ return state;
+ }
+}
+
+function groups(state: GroupsState = {}, action: Action) {
+ if (action.type === 'RECEIVE_ORGANIZATION_GROUPS') {
+ return { ...state, [action.key]: action.groups };
+ }
+ return state;
+}
+
+export default combineReducers({ byKey, my, groups });
+
+export function getOrganizationByKey(state: State, key: string): Organization | undefined {
+ return state.byKey[key];
+}
+
+export function getOrganizationGroupsByKey(state: State, key: string): Group[] {
+ return state.groups[key] || [];
+}
+
+export function getMyOrganizations(state: State): Organization[] {
+ return state.my.map(key => getOrganizationByKey(state, key) as Organization);
+}
+
+export function areThereCustomOrganizations(state: State): boolean {
+ return Object.keys(state.byKey).length > 1;
+}