]> source.dussan.org Git - sonarqube.git/commitdiff
SONARCLOUD-101 Display only organization on which we have correct permissions in...
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>
Fri, 26 Oct 2018 08:14:59 +0000 (10:14 +0200)
committerSonarTech <sonartech@sonarsource.com>
Fri, 16 Nov 2018 19:21:05 +0000 (20:21 +0100)
* Use organizations actions instead of permissions flags from api/navigation/organization

52 files changed:
server/sonar-server/src/main/java/org/sonar/server/organization/ws/SearchAction.java
server/sonar-server/src/main/java/org/sonar/server/ui/ws/OrganizationAction.java
server/sonar-server/src/main/resources/org/sonar/server/organization/ws/search-example.json
server/sonar-server/src/main/resources/org/sonar/server/ui/ws/organization-example.json
server/sonar-server/src/test/java/org/sonar/server/organization/ws/SearchActionTest.java
server/sonar-server/src/test/java/org/sonar/server/ui/ws/OrganizationActionTest.java
server/sonar-web/src/main/js/api/organizations.ts
server/sonar-web/src/main/js/app/components/extensions/OrganizationPageExtension.tsx
server/sonar-web/src/main/js/app/types.ts
server/sonar-web/src/main/js/apps/account/organizations/OrganizationCard.tsx
server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx
server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx
server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CreateOrganization-test.tsx.snap
server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPage-test.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap
server/sonar-web/src/main/js/apps/organizationMembers/MembersListItem.tsx
server/sonar-web/src/main/js/apps/organizationMembers/OrganizationMembers.tsx
server/sonar-web/src/main/js/apps/organizationMembers/__tests__/MembersListItem-test.tsx
server/sonar-web/src/main/js/apps/organizationMembers/__tests__/OrganizationMembers-test.tsx
server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/OrganizationMembers-test.tsx.snap
server/sonar-web/src/main/js/apps/organizations/components/OrganizationAccessContainer.tsx
server/sonar-web/src/main/js/apps/organizations/components/OrganizationPage.tsx
server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationAccessContainer-test.tsx
server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationPage-test.tsx
server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationAccessContainer-test.tsx.snap
server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationPage-test.tsx.snap
server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationAdministration.tsx
server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationMenuContainer.tsx
server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationHeader-test.tsx
server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigationMenuContainer-test.tsx
server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap
server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationMenuContainer-test.tsx.snap
server/sonar-web/src/main/js/apps/projects/components/EmptyInstance.tsx
server/sonar-web/src/main/js/apps/projects/components/__tests__/EmptyInstance-test.tsx
server/sonar-web/src/main/js/apps/projects/components/__tests__/NoFavoriteProjects-test.tsx
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/NoFavoriteProjects-test.tsx.snap
server/sonar-web/src/main/js/apps/projectsManagement/AppContainer.tsx
server/sonar-web/src/main/js/apps/projectsManagement/ChangeVisibilityForm.tsx
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ChangeVisibilityForm-test.tsx
server/sonar-web/src/main/js/apps/tutorials/components/OrganizationStep.tsx
server/sonar-web/src/main/js/apps/tutorials/components/__tests__/OrganizationStep-test.tsx
server/sonar-web/src/main/js/components/common/PrivacyBadgeContainer.tsx
server/sonar-web/src/main/js/components/common/__tests__/PrivacyBadgeContainer-test.tsx
server/sonar-web/src/main/js/components/ui/OrganizationListItem.tsx
server/sonar-web/src/main/js/components/ui/__tests__/OrganizationListItem-test.tsx
server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/OrganizationListItem-test.tsx.snap
server/sonar-web/src/main/js/helpers/__tests__/organizations-test.ts
server/sonar-web/src/main/js/helpers/organizations.ts
server/sonar-web/src/main/js/store/__tests__/__snapshots__/organizations-test.ts.snap
server/sonar-web/src/main/js/store/organizations.ts
sonar-ws/src/main/protobuf/ws-organizations.proto

index 625a16687d7070ed4d697f840dcfc889d5799b69..2efee16b3a5294fcf58540595418b397209fdfbf 100644 (file)
@@ -45,6 +45,7 @@ import static org.sonar.core.util.stream.MoreCollectors.toSet;
 import static org.sonar.db.Pagination.forPage;
 import static org.sonar.db.organization.OrganizationQuery.newOrganizationQueryBuilder;
 import static org.sonar.db.permission.OrganizationPermission.ADMINISTER;
+import static org.sonar.db.permission.OrganizationPermission.PROVISION_PROJECTS;
 import static org.sonar.server.ws.WsUtils.writeProtobuf;
 import static org.sonarqube.ws.Common.Paging;
 
@@ -72,6 +73,7 @@ public class SearchAction implements OrganizationsWsAction {
       .setInternal(true)
       .setSince("6.2")
       .setChangelog(new Change("6.4", "Paging fields have been added to the response"))
+      .setChangelog(new Change("7.5", "Removed 'isAdmin' and return 'actions' for each organization"))
       .setHandler(this);
 
     action.createParam(PARAM_ORGANIZATIONS)
@@ -103,9 +105,10 @@ public class SearchAction implements OrganizationsWsAction {
       Paging paging = buildWsPaging(request, total);
       List<OrganizationDto> organizations = dbClient.organizationDao().selectByQuery(dbSession, dbQuery, forPage(paging.getPageIndex()).andSize(paging.getPageSize()));
       Set<String> adminOrganizationUuids = searchOrganizationWithAdminPermission(dbSession);
+      Set<String> provisionOrganizationUuids = searchOrganizationWithProvisionPermission(dbSession);
       Map<String, OrganizationAlmBindingDto> organizationAlmBindingByOrgUuid = dbClient.organizationAlmBindingDao().selectByOrganizations(dbSession, organizations)
         .stream().collect(MoreCollectors.uniqueIndex(OrganizationAlmBindingDto::getOrganizationUuid));
-      writeResponse(request, response, organizations, adminOrganizationUuids, organizationAlmBindingByOrgUuid, paging);
+      writeResponse(request, response, organizations, adminOrganizationUuids, provisionOrganizationUuids, organizationAlmBindingByOrgUuid, paging);
     }
   }
 
@@ -122,7 +125,14 @@ public class SearchAction implements OrganizationsWsAction {
       : dbClient.organizationDao().selectByPermission(dbSession, userId, ADMINISTER.getKey()).stream().map(OrganizationDto::getUuid).collect(toSet());
   }
 
-  private static void writeResponse(Request httpRequest, Response httpResponse, List<OrganizationDto> organizations, Set<String> adminOrganizationUuids,
+  private Set<String> searchOrganizationWithProvisionPermission(DbSession dbSession) {
+    Integer userId = userSession.getUserId();
+    return userId == null ? emptySet()
+      : dbClient.organizationDao().selectByPermission(dbSession, userId, PROVISION_PROJECTS.getKey()).stream().map(OrganizationDto::getUuid).collect(toSet());
+  }
+
+  private void writeResponse(Request httpRequest, Response httpResponse, List<OrganizationDto> organizations,
+    Set<String> adminOrganizationUuids, Set<String> provisionOrganizationUuids,
     Map<String, OrganizationAlmBindingDto> organizationAlmBindingByOrgUuid,
     Paging paging) {
     Organizations.SearchWsResponse.Builder response = Organizations.SearchWsResponse.newBuilder();
@@ -131,8 +141,12 @@ public class SearchAction implements OrganizationsWsAction {
     organizations
       .forEach(o -> {
         wsOrganization.clear();
-        boolean isAdmin = adminOrganizationUuids.contains(o.getUuid());
-        wsOrganization.setIsAdmin(isAdmin);
+        boolean isAdmin = userSession.isRoot() || adminOrganizationUuids.contains(o.getUuid());
+        boolean canProvision = userSession.isRoot() || provisionOrganizationUuids.contains(o.getUuid());
+        wsOrganization.setActions(Organization.Actions.newBuilder()
+          .setAdmin(isAdmin)
+          .setProvision(canProvision)
+          .setDelete(o.isGuarded() ? userSession.isRoot() : isAdmin));
         response.addOrganizations(toOrganization(wsOrganization, o, organizationAlmBindingByOrgUuid.get(o.getUuid())));
       });
     writeProtobuf(response.build(), httpRequest, httpResponse);
index 5e55a2a3de2eddadbac05f807889f054774456ac..caf938d3dd6c4070a9aa0e1f06dc1e43d0eaeabe 100644 (file)
@@ -40,7 +40,6 @@ import org.sonar.server.user.UserSession;
 
 import static org.sonar.db.organization.OrganizationDto.Subscription.PAID;
 import static org.sonar.db.permission.OrganizationPermission.ADMINISTER;
-import static org.sonar.db.permission.OrganizationPermission.PROVISION_PROJECTS;
 import static org.sonar.server.ws.KeyExamples.KEY_ORG_EXAMPLE_001;
 import static org.sonar.server.ws.WsUtils.checkFoundWithOptional;
 
@@ -114,9 +113,6 @@ public class OrganizationAction implements NavigationWsAction {
       .prop("isDefault", organization.getKey().equals(defaultOrganizationProvider.get().getKey()))
       .prop("projectVisibility", Visibility.getLabel(newProjectPrivate))
       .prop("subscription", organization.getSubscription().name())
-      .prop("canAdmin", userSession.hasPermission(ADMINISTER, organization))
-      .prop("canProvisionProjects", userSession.hasPermission(PROVISION_PROJECTS, organization))
-      .prop("canDelete", organization.isGuarded() ? userSession.isSystemAdministrator() : userSession.hasPermission(ADMINISTER, organization))
       .prop("canUpdateProjectsVisibilityToPrivate",
         userSession.hasPermission(ADMINISTER, organization) &&
           billingValidations.canUpdateProjectVisibilityToPrivate(new BillingValidations.Organization(organization.getKey(), organization.getUuid())));
index 481ae34e9ef10b209aa2265a699857d874cd17cc..a5eeaf171b64c67b0234e614b8ace40fd668be67 100644 (file)
@@ -9,7 +9,11 @@
       "key": "foo-company",
       "name": "Foo Company",
       "guarded": true,
-      "isAdmin": false
+      "actions": {
+        "admin": false,
+        "delete": false,
+        "provision": false
+      }
     },
     {
       "key": "bar-company",
       "url": "https://www.bar.com",
       "avatar": "https://www.bar.com/logo.png",
       "guarded": false,
-      "isAdmin": true
+      "actions": {
+        "admin": true,
+        "delete": true,
+        "provision": false
+      }
     }
   ]
 }
index 48642de61c528ab3b4d50397fbd58c551d43aedc..1f3cc604bb861fc86d80e1770cc13aab0652ec4a 100644 (file)
@@ -1,8 +1,5 @@
 {
   "organization": {
-    "canAdmin": true,
-    "canProvisionProjects": true,
-    "canDelete": false,
     "pages": [
       {
         "key": "my-plugin/org-page",
index 7d54fcb892a075b8e05cafb010c7e9f6ae9b5ce9..94cfe88ccb46061b0186389c94593cfe139f8908 100644 (file)
@@ -56,6 +56,7 @@ import static org.mockito.Mockito.when;
 import static org.sonar.db.organization.OrganizationDto.Subscription.FREE;
 import static org.sonar.db.organization.OrganizationDto.Subscription.PAID;
 import static org.sonar.db.permission.OrganizationPermission.ADMINISTER;
+import static org.sonar.db.permission.OrganizationPermission.PROVISION_PROJECTS;
 import static org.sonar.server.organization.ws.SearchAction.PARAM_MEMBER;
 import static org.sonar.test.JsonAssert.assertJson;
 
@@ -76,23 +77,65 @@ public class SearchActionTest {
   private WsActionTester ws = new WsActionTester(underTest);
 
   @Test
-  public void is_admin_available_for_each_organization() {
+  public void admin_and_delete_action_available_for_each_organization() {
     OrganizationDto userAdminOrganization = db.organizations().insert();
     OrganizationDto groupAdminOrganization = db.organizations().insert();
     OrganizationDto browseOrganization = db.organizations().insert();
+    OrganizationDto guardedOrganization = db.organizations().insert(dto -> dto.setGuarded(true));
     UserDto user = db.users().insertUser();
     GroupDto group = db.users().insertGroup(groupAdminOrganization);
     db.users().insertMember(group, user);
-    userSession.logIn(user).addPermission(ADMINISTER, userAdminOrganization);
+    userSession.logIn(user).addPermission(ADMINISTER, userAdminOrganization)
+      .addPermission(ADMINISTER, guardedOrganization);
     db.users().insertPermissionOnUser(userAdminOrganization, user, ADMINISTER);
+    db.users().insertPermissionOnUser(guardedOrganization, user, ADMINISTER);
     db.users().insertPermissionOnGroup(group, ADMINISTER);
 
     SearchWsResponse result = call(ws.newRequest());
 
-    assertThat(result.getOrganizationsList()).extracting(Organization::getKey, Organization::getIsAdmin).containsExactlyInAnyOrder(
-      tuple(userAdminOrganization.getKey(), true),
+    assertThat(result.getOrganizationsList())
+      .extracting(Organization::getKey, o -> o.getActions().getAdmin(), o -> o.getActions().getDelete())
+      .containsExactlyInAnyOrder(
+        tuple(userAdminOrganization.getKey(), true, true),
+        tuple(browseOrganization.getKey(), false, false),
+        tuple(groupAdminOrganization.getKey(), true, true),
+        tuple(guardedOrganization.getKey(), true, false));
+  }
+
+  @Test
+  public void root_can_do_everything() {
+    OrganizationDto organization = db.organizations().insert();
+    OrganizationDto guardedOrganization = db.organizations().insert(dto -> dto.setGuarded(true));
+    UserDto user = db.users().insertUser();
+    userSession.logIn(user).setRoot();
+
+    SearchWsResponse result = call(ws.newRequest());
+
+    assertThat(result.getOrganizationsList())
+      .extracting(Organization::getKey, o -> o.getActions().getAdmin(), o -> o.getActions().getDelete(), o -> o.getActions().getProvision())
+      .containsExactlyInAnyOrder(
+        tuple(organization.getKey(), true, true, true),
+        tuple(guardedOrganization.getKey(), true, true, true));
+  }
+
+  @Test
+  public void provision_action_available_for_each_organization() {
+    OrganizationDto userProvisionOrganization = db.organizations().insert();
+    OrganizationDto groupProvisionOrganization = db.organizations().insert();
+    OrganizationDto browseOrganization = db.organizations().insert();
+    UserDto user = db.users().insertUser();
+    GroupDto group = db.users().insertGroup(groupProvisionOrganization);
+    db.users().insertMember(group, user);
+    userSession.logIn(user).addPermission(PROVISION_PROJECTS, userProvisionOrganization);
+    db.users().insertPermissionOnUser(userProvisionOrganization, user, PROVISION_PROJECTS);
+    db.users().insertPermissionOnGroup(group, PROVISION_PROJECTS);
+
+    SearchWsResponse result = call(ws.newRequest());
+
+    assertThat(result.getOrganizationsList()).extracting(Organization::getKey, o -> o.getActions().getProvision()).containsExactlyInAnyOrder(
+      tuple(userProvisionOrganization.getKey(), true),
       tuple(browseOrganization.getKey(), false),
-      tuple(groupAdminOrganization.getKey(), true));
+      tuple(groupProvisionOrganization.getKey(), true));
   }
 
   @Test
index 80e68c5b3501b5764c811e4e62b6b7d3bfe84c46..01dcf780d63ecca3df8b84cb38023d7cea23d02c 100644 (file)
@@ -90,91 +90,6 @@ public class OrganizationActionTest {
       .doesNotContain("my-plugin/org-admin-page");
   }
 
-  @Test
-  public void returns_non_admin_and_canDelete_false_when_user_not_logged_in_and_key_is_the_default_organization() {
-    TestResponse response = executeRequest(db.getDefaultOrganization());
-
-    verifyResponse(response, false, false, false);
-  }
-
-  @Test
-  public void returns_non_admin_and_canDelete_false_when_user_logged_in_but_not_admin_and_key_is_the_default_organization() {
-    userSession.logIn();
-
-    TestResponse response = executeRequest(db.getDefaultOrganization());
-
-    verifyResponse(response, false, false, false);
-  }
-
-  @Test
-  public void returns_admin_and_canDelete_true_when_user_logged_in_and_admin_and_key_is_the_default_organization() {
-    OrganizationDto defaultOrganization = db.getDefaultOrganization();
-    userSession.logIn().addPermission(ADMINISTER, defaultOrganization);
-
-    TestResponse response = executeRequest(defaultOrganization);
-
-    verifyResponse(response, true, false, true);
-  }
-
-  @Test
-  public void returns_non_admin_and_canDelete_false_when_user_not_logged_in_and_key_is_not_the_default_organization() {
-    OrganizationDto organization = db.organizations().insert();
-    TestResponse response = executeRequest(organization);
-
-    verifyResponse(response, false, false, false);
-  }
-
-  @Test
-  public void returns_non_admin_and_canDelete_false_when_user_logged_in_but_not_admin_and_key_is_not_the_default_organization() {
-    OrganizationDto organization = db.organizations().insert();
-    userSession.logIn();
-
-    TestResponse response = executeRequest(organization);
-
-    verifyResponse(response, false, false, false);
-  }
-
-  @Test
-  public void returns_admin_and_canDelete_true_when_user_logged_in_and_admin_and_key_is_not_the_default_organization() {
-    OrganizationDto organization = db.organizations().insert();
-    userSession.logIn().addPermission(ADMINISTER, organization);
-
-    TestResponse response = executeRequest(organization);
-
-    verifyResponse(response, true, false, true);
-  }
-
-  @Test
-  public void returns_admin_and_canDelete_false_when_user_logged_in_and_admin_and_key_is_guarded_organization() {
-    OrganizationDto organization = db.organizations().insert(dto -> dto.setGuarded(true));
-    userSession.logIn().addPermission(ADMINISTER, organization);
-
-    TestResponse response = executeRequest(organization);
-
-    verifyResponse(response, true, false, false);
-  }
-
-  @Test
-  public void returns_only_canDelete_true_when_user_is_system_administrator_and_key_is_guarded_organization() {
-    OrganizationDto organization = db.organizations().insert(dto -> dto.setGuarded(true));
-    userSession.logIn().setSystemAdministrator();
-
-    TestResponse response = executeRequest(organization);
-
-    verifyResponse(response, false, false, true);
-  }
-
-  @Test
-  public void returns_provisioning_true_when_user_can_provision_projects_in_organization() {
-    // user can provision projects in org2 but not in org1
-    OrganizationDto org1 = db.organizations().insert();
-    OrganizationDto org2 = db.organizations().insert();
-    userSession.logIn().addPermission(PROVISION_PROJECTS, org2);
-
-    verifyResponse(executeRequest(org1), false, false, false);
-    verifyResponse(executeRequest(org2), false, true, false);
-  }
-
   @Test
   public void returns_project_visibility_private() {
     OrganizationDto organization = db.organizations().insert();
@@ -326,18 +241,6 @@ public class OrganizationActionTest {
     return request.execute();
   }
 
-  private static void verifyResponse(TestResponse response, boolean canAdmin, boolean canProvisionProjects, boolean canDelete) {
-    assertJson(response.getInput())
-      .isSimilarTo("{" +
-        "  \"organization\": {" +
-        "    \"canAdmin\": " + canAdmin + "," +
-        "    \"canProvisionProjects\": " + canProvisionProjects + "," +
-        "    \"canDelete\": " + canDelete +
-        "    \"pages\": []" +
-        "  }" +
-        "}");
-  }
-
   private static void verifyCanUpdateProjectsVisibilityToPrivateResponse(TestResponse response, boolean canUpdateProjectsVisibilityToPrivate) {
     assertJson(response.getInput())
       .isSimilarTo("{" +
index 3ed1fe0c87c53d7230b0ad17d4544727ee3633ce..79397d3b5a202355a6af29f0bbcb69b201f7aca4 100644 (file)
  */
 import { getJSON, post, postJSON } from '../helpers/request';
 import throwGlobalError from '../app/utils/throwGlobalError';
-import { Organization, OrganizationBase, Paging, OrganizationMember } from '../app/types';
+import {
+  Organization,
+  OrganizationBase,
+  Paging,
+  OrganizationMember,
+  Extension
+} from '../app/types';
 
 export function getOrganizations(data: {
   organizations?: string;
@@ -39,12 +45,10 @@ export function getOrganization(key: string): Promise<Organization | undefined>
 }
 
 interface GetOrganizationNavigation {
-  adminPages: Array<{ key: string; name: string }>;
-  canAdmin: boolean;
-  canDelete: boolean;
-  canProvisionProjects: boolean;
+  adminPages: Extension[];
+  canUpdateProjectsVisibilityToPrivate: boolean;
   isDefault: boolean;
-  pages: Array<{ key: string; name: string }>;
+  pages: Extension[];
 }
 
 export function getOrganizationNavigation(key: string): Promise<GetOrganizationNavigation> {
index d1b1b181a8fde6a7a6c801b1f96a1b2082f0ba9c..56eef2983dc0b03e8af6c7fb3161db7f0ee12751 100644 (file)
@@ -56,8 +56,9 @@ class OrganizationPageExtension extends React.PureComponent<Props> {
       return null;
     }
 
-    let pages = organization.pages || [];
-    if (organization.canAdmin && organization.adminPages) {
+    const { actions = {} } = organization;
+    let { pages = [] } = organization;
+    if (actions.admin && organization.adminPages) {
       pages = pages.concat(organization.adminPages);
     }
 
index e5b615e42d59c3287c86a47151e8560bb55a34f3..37b1bf0734578248a4718c9ec6549e1c03845bb0 100644 (file)
@@ -485,15 +485,19 @@ export interface Notification {
   type: string;
 }
 
+export interface OrganizationActions {
+  admin?: boolean;
+  delete?: boolean;
+  provision?: boolean;
+  executeAnalysis?: boolean;
+}
+
 export interface Organization extends OrganizationBase {
+  actions?: OrganizationActions;
   alm?: { key: string; url: string };
   adminPages?: Extension[];
-  canAdmin?: boolean;
-  canDelete?: boolean;
-  canProvisionProjects?: boolean;
   canUpdateProjectsVisibilityToPrivate?: boolean;
   guarded?: boolean;
-  isAdmin?: boolean;
   isDefault?: boolean;
   key: string;
   pages?: Extension[];
index db53d626a83d33ce130a579311348547208da26e..99cc1b00272056e8b4b02aa2db52c7595e3a3e99 100644 (file)
@@ -28,6 +28,7 @@ interface Props {
 }
 
 export default function OrganizationCard({ organization }: Props) {
+  const { actions = {} } = organization;
   return (
     <div className="account-project-card clearfix">
       <aside className="account-project-side note">
@@ -39,9 +40,7 @@ export default function OrganizationCard({ organization }: Props) {
         <OrganizationLink className="spacer-left text-middle" organization={organization}>
           {organization.name}
         </OrganizationLink>
-        {organization.isAdmin && (
-          <span className="outline-badge spacer-left">{translate('admin')}</span>
-        )}
+        {actions.admin && <span className="outline-badge spacer-left">{translate('admin')}</span>}
       </h3>
 
       {!!organization.description && (
index 5357076557b8dc5a4768ea626b078069621a6841..44be6b5bcc8dde4cbae53bbe69c06c50265b9558 100644 (file)
@@ -290,7 +290,8 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr
             createOrganization={this.props.createOrganization}
             onOrgCreated={this.handleOrgCreated}
             unboundOrganizations={this.props.userOrganizations.filter(
-              o => !o.alm && o.key !== currentUser.personalOrganization
+              ({ actions = {}, alm, key }) =>
+                !alm && key !== currentUser.personalOrganization && actions.admin
             )}
           />
         )}
index 09d55dcf6b1f96aaae9211641243195b1716facf..38e0c5bb65b09451eeeb9e3974c56e6e516172f3 100644 (file)
@@ -249,8 +249,9 @@ function shallowRender(props: Partial<CreateOrganization['props']> = {}) {
       skipOnboardingAction={jest.fn()}
       updateOrganization={jest.fn()}
       userOrganizations={[
-        { key: 'foo', name: 'Foo' },
-        { alm: { key: 'github', url: '' }, key: 'bar', name: 'Bar' }
+        { actions: { admin: true }, key: 'foo', name: 'Foo' },
+        { actions: { admin: true }, alm: { key: 'github', url: '' }, key: 'bar', name: 'Bar' },
+        { actions: { admin: false }, key: 'baz', name: 'Baz' }
       ]}
       {...props}
     />
index e7b7edca442fc9e794898b8a351a06d3a994ddb6..748f5e88a96578d4088202cd3e93c3a63f928afe 100644 (file)
@@ -69,6 +69,9 @@ exports[`should render with auto personal organization bind page 2`] = `
       }
       importPersonalOrg={
         Object {
+          "actions": Object {
+            "admin": true,
+          },
           "key": "foo",
           "name": "Foo",
         }
@@ -155,6 +158,9 @@ exports[`should render with auto tab displayed 1`] = `
       unboundOrganizations={
         Array [
           Object {
+            "actions": Object {
+              "admin": true,
+            },
             "key": "foo",
             "name": "Foo",
           },
@@ -257,6 +263,9 @@ exports[`should render with auto tab selected and manual disabled 2`] = `
       unboundOrganizations={
         Array [
           Object {
+            "actions": Object {
+              "admin": true,
+            },
             "key": "foo",
             "name": "Foo",
           },
@@ -405,6 +414,9 @@ exports[`should switch tabs 1`] = `
       unboundOrganizations={
         Array [
           Object {
+            "actions": Object {
+              "admin": true,
+            },
             "key": "foo",
             "name": "Foo",
           },
index 7afb0d1ca1012abbd1225dbb15ac03ea1ada6fcd..2338c3414b855f77c3e142cba06ead0745100a34 100644 (file)
@@ -27,10 +27,9 @@ import DeferredSpinner from '../../../components/common/DeferredSpinner';
 import Tabs from '../../../components/controls/Tabs';
 import { whenLoggedIn } from '../../../components/hoc/whenLoggedIn';
 import { withUserOrganizations } from '../../../components/hoc/withUserOrganizations';
-import { skipOnboarding as skipOnboardingAction } from '../../../store/users';
+import { skipOnboarding } from '../../../store/users';
 import { LoggedInUser, AlmApplication, Organization } from '../../../app/types';
 import { getAlmAppInfo } from '../../../api/alm-integration';
-import { skipOnboarding } from '../../../api/users';
 import { hasAdvancedALMIntegration } from '../../../helpers/almIntegrations';
 import { translate } from '../../../helpers/l10n';
 import { getProjectUrl } from '../../../helpers/urls';
@@ -38,7 +37,7 @@ import '../../../app/styles/sonarcloud.css';
 
 interface Props {
   currentUser: LoggedInUser;
-  skipOnboardingAction: () => void;
+  skipOnboarding: () => void;
   userOrganizations: Organization[];
 }
 
@@ -80,8 +79,7 @@ export class CreateProjectPage extends React.PureComponent<Props & WithRouterPro
   }
 
   handleProjectCreate = (projectKeys: string[]) => {
-    skipOnboarding().catch(() => {});
-    this.props.skipOnboardingAction();
+    this.props.skipOnboarding();
     if (projectKeys.length > 1) {
       this.props.router.push({ pathname: '/projects' });
     } else if (projectKeys.length === 1) {
@@ -153,12 +151,16 @@ export class CreateProjectPage extends React.PureComponent<Props & WithRouterPro
                   currentUser={currentUser}
                   onProjectCreate={this.handleProjectCreate}
                   organization={state.organization}
-                  userOrganizations={userOrganizations}
+                  userOrganizations={userOrganizations.filter(
+                    ({ actions = {} }) => actions.provision
+                  )}
                 />
               ) : (
                 <AutoProjectCreate
                   almApplication={almApplication}
-                  boundOrganizations={userOrganizations.filter(o => o.alm)}
+                  boundOrganizations={userOrganizations.filter(
+                    ({ alm, actions = {} }) => alm && actions.provision
+                  )}
                   onProjectCreate={this.handleProjectCreate}
                   organization={state.organization}
                 />
@@ -171,7 +173,7 @@ export class CreateProjectPage extends React.PureComponent<Props & WithRouterPro
   }
 }
 
-const mapDispatchToProps = { skipOnboardingAction };
+const mapDispatchToProps = { skipOnboarding };
 
 export default whenLoggedIn(
   withUserOrganizations(
index 12af30eb2b8fbbddbd0c80da9aea48d1c7ce7466..95f3a5eabcb792ad691c65cfa8d308427ab873e0 100644 (file)
@@ -84,10 +84,11 @@ function getWrapper(props = {}) {
       // @ts-ignore avoid passing everything from WithRouterProps
       location={{}}
       router={mockRouter()}
-      skipOnboardingAction={jest.fn()}
+      skipOnboarding={jest.fn()}
       userOrganizations={[
-        { key: 'foo', name: 'Foo' },
-        { alm: { key: 'github', url: '' }, key: 'bar', name: 'Bar' }
+        { actions: { provision: true }, key: 'foo', name: 'Foo' },
+        { actions: { provision: true }, alm: { key: 'github', url: '' }, key: 'bar', name: 'Bar' },
+        { actions: { provision: false }, key: 'baz', name: 'Baz' }
       ]}
       {...props}
     />
index c0ff4ce75353fd34dcf0a0605ae86a8ec39ffc61..99e93d8f31715be4f85b685049f6ddea440b5444 100644 (file)
@@ -76,6 +76,9 @@ exports[`should render correctly 2`] = `
       boundOrganizations={
         Array [
           Object {
+            "actions": Object {
+              "provision": true,
+            },
             "alm": Object {
               "key": "github",
               "url": "",
@@ -126,10 +129,16 @@ exports[`should render with Manual creation only 1`] = `
       userOrganizations={
         Array [
           Object {
+            "actions": Object {
+              "provision": true,
+            },
             "key": "foo",
             "name": "Foo",
           },
           Object {
+            "actions": Object {
+              "provision": true,
+            },
             "alm": Object {
               "key": "github",
               "url": "",
@@ -193,6 +202,9 @@ exports[`should switch tabs 1`] = `
       boundOrganizations={
         Array [
           Object {
+            "actions": Object {
+              "provision": true,
+            },
             "alm": Object {
               "key": "github",
               "url": "",
index 43b2535d68f2145a400e95b8681d3545d7476c20..4b586b366d9cd7730659cd3593138a5b993eacc8 100644 (file)
@@ -78,6 +78,7 @@ export default class MembersListItem extends React.PureComponent<Props, State> {
 
   render() {
     const { member, organization } = this.props;
+    const { actions = {} } = organization;
     return (
       <tr>
         <td className="thin nowrap">
@@ -87,7 +88,7 @@ export default class MembersListItem extends React.PureComponent<Props, State> {
           <strong>{member.name}</strong>
           <span className="note little-spacer-left">{member.login}</span>
         </td>
-        {organization.canAdmin && (
+        {actions.admin && (
           <td className="text-right text-middle">
             {translateWithParameters(
               'organization.members.x_groups',
@@ -95,7 +96,7 @@ export default class MembersListItem extends React.PureComponent<Props, State> {
             )}
           </td>
         )}
-        {organization.canAdmin && (
+        {actions.admin && (
           <React.Fragment>
             <td className="nowrap text-middle text-right">
               <ActionsDropdown>
index 5acb076d0535dae4f33defabd8bc9d3a5fba6750..1769a76333a544518957ce8cad547fa07d0acd53 100644 (file)
@@ -56,7 +56,7 @@ export default class OrganizationMembers extends React.PureComponent<Props, Stat
   componentDidMount() {
     this.mounted = true;
     this.fetchMembers();
-    if (this.props.organization.canAdmin) {
+    if (this.props.organization.actions && this.props.organization.actions.admin) {
       this.fetchGroups();
     }
   }
@@ -191,19 +191,20 @@ export default class OrganizationMembers extends React.PureComponent<Props, Stat
         <Helmet title={translate('organization.members.page')} />
         <Suggestions suggestions="organization_members" />
         <MembersPageHeader loading={loading}>
-          {organization.canAdmin && (
-            <div className="page-actions">
-              <AddMemberForm
-                addMember={this.handleAddMember}
-                memberLogins={memberLogins}
-                organization={organization}
-              />
-              <DocTooltip
-                className="spacer-left"
-                doc={import(/* webpackMode: "eager" */ 'Docs/tooltips/organizations/add-organization-member.md')}
-              />
-            </div>
-          )}
+          {organization.actions &&
+            organization.actions.admin && (
+              <div className="page-actions">
+                <AddMemberForm
+                  addMember={this.handleAddMember}
+                  memberLogins={memberLogins}
+                  organization={organization}
+                />
+                <DocTooltip
+                  className="spacer-left"
+                  doc={import(/* webpackMode: "eager" */ 'Docs/tooltips/organizations/add-organization-member.md')}
+                />
+              </div>
+            )}
         </MembersPageHeader>
         {members !== undefined &&
           paging !== undefined && (
index 18c91ecec6194d806a6218129ccde50079808a76..13e9c498eafde75444021ca42381414a0428339d 100644 (file)
@@ -43,7 +43,7 @@ it('should render actions and groups for admin', () => {
   const wrapper = shallow(
     <MembersListItem
       member={admin}
-      organization={{ ...organization, canAdmin: true }}
+      organization={{ ...organization, actions: { admin: true } }}
       organizationGroups={[]}
       removeMember={jest.fn()}
       updateMemberGroups={jest.fn()}
@@ -56,7 +56,7 @@ it('should groups at 0 if the groupCount field is not defined (just added user)'
   const wrapper = shallow(
     <MembersListItem
       member={john}
-      organization={{ ...organization, canAdmin: true }}
+      organization={{ ...organization, actions: { admin: true } }}
       organizationGroups={[]}
       removeMember={jest.fn()}
       updateMemberGroups={jest.fn()}
@@ -69,7 +69,7 @@ it('should open groups form', () => {
   const wrapper = shallow(
     <MembersListItem
       member={admin}
-      organization={{ ...organization, canAdmin: true }}
+      organization={{ ...organization, actions: { admin: true } }}
       organizationGroups={[]}
       removeMember={jest.fn()}
       updateMemberGroups={jest.fn()}
@@ -88,7 +88,7 @@ it('should open remove member form', () => {
   const wrapper = shallow(
     <MembersListItem
       member={admin}
-      organization={{ ...organization, canAdmin: true }}
+      organization={{ ...organization, actions: { admin: true } }}
       organizationGroups={[]}
       removeMember={jest.fn()}
       updateMemberGroups={jest.fn()}
index d60b0e9c3c094eab974b06f4be9b4cf591d5cc8a..429e6d3476710528e9121295d076318e64634685 100644 (file)
@@ -67,7 +67,7 @@ it('should fetch members and render for non-admin', async () => {
 
 it('should fetch members and groups and render for admin', async () => {
   const wrapper = shallow(
-    <OrganizationMembers organization={{ ...organization, canAdmin: true }} />
+    <OrganizationMembers organization={{ ...organization, actions: { admin: true } }} />
   );
   await waitAndUpdate(wrapper);
   expect(wrapper).toMatchSnapshot();
@@ -77,7 +77,7 @@ it('should fetch members and groups and render for admin', async () => {
 
 it('should search users', async () => {
   const wrapper = shallow(
-    <OrganizationMembers organization={{ ...organization, canAdmin: true }} />
+    <OrganizationMembers organization={{ ...organization, actions: { admin: true } }} />
   );
   await waitAndUpdate(wrapper);
   wrapper.find('MembersListHeader').prop<Function>('handleSearch')('user');
@@ -86,7 +86,7 @@ it('should search users', async () => {
 
 it('should load more members', async () => {
   const wrapper = shallow(
-    <OrganizationMembers organization={{ ...organization, canAdmin: true }} />
+    <OrganizationMembers organization={{ ...organization, actions: { admin: true } }} />
   );
   await waitAndUpdate(wrapper);
   wrapper.find('ListFooter').prop<Function>('loadMore')();
@@ -95,7 +95,7 @@ it('should load more members', async () => {
 
 it('should add new member', async () => {
   const wrapper = shallow(
-    <OrganizationMembers organization={{ ...organization, canAdmin: true }} />
+    <OrganizationMembers organization={{ ...organization, actions: { admin: true } }} />
   );
   await waitAndUpdate(wrapper);
   wrapper.find('AddMemberForm').prop<Function>('addMember')({ login: 'bar' });
@@ -112,7 +112,7 @@ it('should add new member', async () => {
 
 it('should remove member', async () => {
   const wrapper = shallow(
-    <OrganizationMembers organization={{ ...organization, canAdmin: true }} />
+    <OrganizationMembers organization={{ ...organization, actions: { admin: true } }} />
   );
   await waitAndUpdate(wrapper);
   wrapper.find('MembersList').prop<Function>('removeMember')({ login: 'john' });
@@ -129,7 +129,7 @@ it('should remove member', async () => {
 
 it('should update groups', async () => {
   const wrapper = shallow(
-    <OrganizationMembers organization={{ ...organization, canAdmin: true }} />
+    <OrganizationMembers organization={{ ...organization, actions: { admin: true } }} />
   );
   await waitAndUpdate(wrapper);
   wrapper.find('MembersList').prop<Function>('updateMemberGroups')(
index bbc33cb4737782919b7433da1439f2698d0ad336..90b44168efcbe9e820967d833c194cc3ce993b9a 100644 (file)
@@ -28,7 +28,9 @@ exports[`should fetch members and groups and render for admin 1`] = `
         }
         organization={
           Object {
-            "canAdmin": true,
+            "actions": Object {
+              "admin": true,
+            },
             "key": "foo",
             "name": "Foo",
           }
@@ -63,7 +65,9 @@ exports[`should fetch members and groups and render for admin 1`] = `
     }
     organization={
       Object {
-        "canAdmin": true,
+        "actions": Object {
+          "admin": true,
+        },
         "key": "foo",
         "name": "Foo",
       }
index 1689dc068961b0fee649d8d6134306a3cdbb3ddb..72263ccf372867f222af32a5ab8d3d7660a8ff92 100644 (file)
@@ -75,8 +75,9 @@ export function hasAdminAccess({
   currentUser,
   organization
 }: Pick<StateToProps, 'currentUser' | 'organization'>) {
-  const isAdmin = isLoggedIn(currentUser) && organization && organization.canAdmin;
-  return Boolean(isAdmin);
+  return Boolean(
+    isLoggedIn(currentUser) && organization && organization.actions && organization.actions.admin
+  );
 }
 
 export function OrganizationAdminAccess(props: OwnProps) {
index 5ae1a3503da126176626f45c1541be1740cf1619..b78929a9040dc453386dbc64dff94738b0b619bc 100644 (file)
@@ -99,7 +99,7 @@ export class OrganizationPage extends React.PureComponent<Props, State> {
   render() {
     const { organization } = this.props;
 
-    if (!organization || organization.canAdmin == null) {
+    if (!organization || !organization.actions || organization.actions.admin == null) {
       if (this.state.loading) {
         return null;
       } else {
index 17b1fd80bb1b8e2b4e7a7bef8c092c4fe6696f7c..961b7f6bc4bee03c7e06d32010ccdcbbbafdd8e5 100644 (file)
@@ -39,13 +39,13 @@ const loggedInUser = {
 };
 
 const organization = {
-  canAdmin: false,
+  actions: { admin: false },
   key: 'foo',
   name: 'Foo',
   projectVisibility: Visibility.Public
 };
 
-const adminOrganization = { ...organization, canAdmin: true };
+const adminOrganization = { ...organization, actions: { admin: true } };
 
 describe('component', () => {
   it('should render children', () => {
index 0f97587fa9d785048c236ca07a1332054b1ff3e8..5af2c96e6fe91ddc03f42095bd738b76c3c628a9 100644 (file)
@@ -32,7 +32,7 @@ it('smoke test', () => {
   const wrapper = getWrapper();
   expect(wrapper.type()).toBeNull();
 
-  const organization = { key: 'foo', name: 'Foo', isDefault: false, canAdmin: false };
+  const organization = { actions: { admin: false }, key: 'foo', name: 'Foo', isDefault: false };
   wrapper.setProps({ organization });
   expect(wrapper).toMatchSnapshot();
 });
index df4447403e654d1c8083d8f531fd96d644dec215..ae075289ee99ff9c4e3c6420e2dd349f7cbba4aa 100644 (file)
@@ -5,7 +5,9 @@ exports[`component should render children 1`] = `
   location={Object {}}
   organization={
     Object {
-      "canAdmin": true,
+      "actions": Object {
+        "admin": true,
+      },
       "key": "foo",
       "name": "Foo",
       "projectVisibility": "public",
index 7c58c886214853df37403fa3b00b5e87b5a834c6..9836ed00ed400c9618ecce87e0cf442d9e69db70 100644 (file)
@@ -41,7 +41,9 @@ exports[`smoke test 1`] = `
     }
     organization={
       Object {
-        "canAdmin": false,
+        "actions": Object {
+          "admin": false,
+        },
         "isDefault": false,
         "key": "foo",
         "name": "Foo",
index 3b6f30e664f85cc47cb100505b1c7bad4611b38a..e890a54eb1168eb24935090b720b3aa61d25f590 100644 (file)
@@ -41,8 +41,8 @@ const ADMIN_PATHS = [
 ];
 
 export default function OrganizationNavigationAdministration({ location, organization }: Props) {
-  const extensions = organization.adminPages || [];
-  const adminPathsWithExtensions = extensions.map(e => `extension/${e.key}`).concat(ADMIN_PATHS);
+  const { actions = {}, adminPages = [] } = organization;
+  const adminPathsWithExtensions = adminPages.map(e => `extension/${e.key}`).concat(ADMIN_PATHS);
   const adminActive = adminPathsWithExtensions.some(path =>
     location.pathname.endsWith(`organizations/${organization.key}/${path}`)
   );
@@ -51,7 +51,7 @@ export default function OrganizationNavigationAdministration({ location, organiz
     <Dropdown
       overlay={
         <ul className="menu">
-          {extensions.map(extension => (
+          {adminPages.map(extension => (
             <li key={extension.key}>
               <Link
                 activeClassName="active"
@@ -94,7 +94,7 @@ export default function OrganizationNavigationAdministration({ location, organiz
               {translate('edit')}
             </Link>
           </li>
-          {organization.canDelete && (
+          {actions.delete && (
             <li>
               <Link activeClassName="active" to={`/organizations/${organization.key}/delete`}>
                 {translate('delete')}
index 518a2a86c52f63bce76e80d855701b1cba41fe93..ba5160c5baac951253a5e8ab0738bb4fb09bac9e 100644 (file)
@@ -48,6 +48,7 @@ export function OrganizationNavigationMenu({
   userOrganizations
 }: Props) {
   const hasPrivateRights = hasPrivateAccess(currentUser, organization, userOrganizations);
+  const { actions = {} } = organization;
   return (
     <NavBarTabs className="navbar-context-tabs">
       <li>
@@ -92,7 +93,7 @@ export function OrganizationNavigationMenu({
         </li>
       )}
       <OrganizationNavigationExtensions location={location} organization={organization} />
-      {organization.canAdmin && (
+      {actions.admin && (
         <OrganizationNavigationAdministration location={location} organization={organization} />
       )}
     </NavBarTabs>
index 2d6853a617d72d6b88a81112f787cac0049802f8..6b08b874b9e9ce24cba8741d355077d636bf7c1a 100644 (file)
@@ -51,8 +51,8 @@ it('renders with alm integration', () => {
 
 it('renders dropdown', () => {
   const organizations = [
-    { isAdmin: true, key: 'org1', name: 'org1', projectVisibility: Visibility.Public },
-    { isAdmin: false, key: 'org2', name: 'org2', projectVisibility: Visibility.Public }
+    { actions: { admin: true }, key: 'org1', name: 'org1', projectVisibility: Visibility.Public },
+    { actions: { admin: false }, key: 'org2', name: 'org2', projectVisibility: Visibility.Public }
   ];
   const wrapper = shallow(
     <OrganizationNavigationHeader
index ee37622b91bf27ebaf19804d3ac4d94907ae7dae..e2c37db5866c606331af582d97e42617b24bb966 100644 (file)
@@ -65,7 +65,7 @@ it('renders for admin', () => {
       <OrganizationNavigationMenu
         currentUser={loggedInUser}
         location={{ pathname: '' }}
-        organization={{ ...organization, canAdmin: true }}
+        organization={{ ...organization, actions: { admin: true } }}
         userOrganizations={[organization]}
       />
     )
index ca3bd2d87cdc6edcb7945c8b1c5ca1994aaf1a02..5ef064c39fc5d768f293d528104c11683114f008 100644 (file)
@@ -31,7 +31,9 @@ exports[`renders dropdown 1`] = `
       <OrganizationListItem
         organization={
           Object {
-            "isAdmin": true,
+            "actions": Object {
+              "admin": true,
+            },
             "key": "org1",
             "name": "org1",
             "projectVisibility": "public",
@@ -41,7 +43,9 @@ exports[`renders dropdown 1`] = `
       <OrganizationListItem
         organization={
           Object {
-            "isAdmin": false,
+            "actions": Object {
+              "admin": false,
+            },
             "key": "org2",
             "name": "org2",
             "projectVisibility": "public",
index 1177b7604b75cbfa75bba0549603d46061a38d9e..46f062c94ae73de5a7ef82d50eada0d3531c00be 100644 (file)
@@ -175,7 +175,9 @@ exports[`renders for admin 1`] = `
     }
     organization={
       Object {
-        "canAdmin": true,
+        "actions": Object {
+          "admin": true,
+        },
         "key": "foo",
         "name": "Foo",
         "projectVisibility": "public",
@@ -190,7 +192,9 @@ exports[`renders for admin 1`] = `
     }
     organization={
       Object {
-        "canAdmin": true,
+        "actions": Object {
+          "admin": true,
+        },
         "key": "foo",
         "name": "Foo",
         "projectVisibility": "public",
index 09668bcd0e5b9f17bbf9b9f67eddb39b75f80090..a4e91c4430209135c3ea552526c976197c890d78 100644 (file)
@@ -42,7 +42,7 @@ export default class EmptyInstance extends React.PureComponent<Props> {
   render() {
     const { currentUser, organization } = this.props;
     const showNewProjectButton = isSonarCloud()
-      ? organization && organization.canProvisionProjects
+      ? organization && organization.actions && organization.actions.provision
       : isLoggedIn(currentUser) && hasGlobalPermission(currentUser, 'provisioning');
 
     return (
index f982f6d7e4882bd355a0bdb84e13f967217c6f30..6cc4deb62f015282244f6eeb9162cb9ffa13815b 100644 (file)
@@ -55,7 +55,7 @@ it('renders correctly for SC', () => {
     shallow(
       <EmptyInstance
         currentUser={{ isLoggedIn: false }}
-        organization={{ canProvisionProjects: true, key: 'foo', name: 'Foo' }}
+        organization={{ actions: { provision: true }, key: 'foo', name: 'Foo' }}
       />
     )
   ).toMatchSnapshot();
index d49eaa626107683438a5557c2b484fa0a8dd67ce..01b0775538848cb1ab8d902404d3b7c73859b309 100644 (file)
@@ -33,8 +33,8 @@ it('renders', () => {
 it('renders for SonarCloud', () => {
   (isSonarCloud as jest.Mock).mockImplementation(() => true);
   const organizations = [
-    { isAdmin: true, key: 'org1', name: 'org1', projectVisibility: Visibility.Public },
-    { isAdmin: false, key: 'org2', name: 'org2', projectVisibility: Visibility.Public }
+    { actions: { admin: true }, key: 'org1', name: 'org1', projectVisibility: Visibility.Public },
+    { actions: { admin: false }, key: 'org2', name: 'org2', projectVisibility: Visibility.Public }
   ];
   expect(shallow(<NoFavoriteProjects organizations={organizations} />)).toMatchSnapshot();
 });
index 1c8cd9c5ba2cb97dd507e959f42398583f535c2e..317c514480e480443b1a1f5ace4c68f2a76125eb 100644 (file)
@@ -59,7 +59,9 @@ exports[`renders for SonarCloud 1`] = `
             <OrganizationListItem
               organization={
                 Object {
-                  "isAdmin": true,
+                  "actions": Object {
+                    "admin": true,
+                  },
                   "key": "org1",
                   "name": "org1",
                   "projectVisibility": "public",
@@ -69,7 +71,9 @@ exports[`renders for SonarCloud 1`] = `
             <OrganizationListItem
               organization={
                 Object {
-                  "isAdmin": false,
+                  "actions": Object {
+                    "admin": false,
+                  },
                   "key": "org2",
                   "name": "org2",
                   "projectVisibility": "public",
index b7ac3c91a5a1d4730cbccb9dd8435a9c16a0de66..a932cbe7b5b9cdc9e8cc8082edca7ceefe7f4a97 100644 (file)
@@ -66,11 +66,12 @@ class AppContainer extends React.PureComponent<OwnProps & StateProps & DispatchP
     }
 
     const topLevelQualifiers = organization.isDefault ? this.props.appState.qualifiers : ['TRK'];
+    const { actions = {} } = organization;
 
     return (
       <App
         currentUser={this.props.currentUser}
-        hasProvisionPermission={organization.canProvisionProjects}
+        hasProvisionPermission={actions.provision}
         onVisibilityChange={this.handleVisibilityChange}
         organization={organization}
         topLevelQualifiers={topLevelQualifiers}
index 15fd04cf537d466793ac159781305b92198ef3d0..55ce40be1b990fe1e000caa0128c8aa78e6cd13a 100644 (file)
@@ -19,9 +19,9 @@
  */
 import * as React from 'react';
 import * as classNames from 'classnames';
-import { Organization, Visibility } from '../../app/types';
 import UpgradeOrganizationBox from '../../components/common/UpgradeOrganizationBox';
 import Modal from '../../components/controls/Modal';
+import { Organization, Visibility } from '../../app/types';
 import { Button, ResetButtonLink } from '../../components/ui/buttons';
 import { translate } from '../../helpers/l10n';
 import { Alert } from '../../components/ui/Alert';
@@ -55,8 +55,7 @@ export default class ChangeVisibilityForm extends React.PureComponent<Props, Sta
   };
 
   render() {
-    const { canUpdateProjectsVisibilityToPrivate } = this.props.organization;
-
+    const { organization } = this.props;
     return (
       <Modal contentLabel="modal form" onRequestClose={this.props.onClose}>
         <header className="modal-head">
@@ -67,7 +66,8 @@ export default class ChangeVisibilityForm extends React.PureComponent<Props, Sta
           {[Visibility.Public, Visibility.Private].map(visibility => (
             <div className="big-spacer-bottom" key={visibility}>
               <p>
-                {visibility === Visibility.Private && !canUpdateProjectsVisibilityToPrivate ? (
+                {visibility === Visibility.Private &&
+                !organization.canUpdateProjectsVisibilityToPrivate ? (
                   <span className="text-muted cursor-not-allowed">
                     <i
                       className={classNames('icon-radio', 'spacer-right', {
@@ -97,7 +97,7 @@ export default class ChangeVisibilityForm extends React.PureComponent<Props, Sta
             </div>
           ))}
 
-          {canUpdateProjectsVisibilityToPrivate ? (
+          {organization.canUpdateProjectsVisibilityToPrivate ? (
             <Alert variant="warning">
               {translate('organization.change_visibility_form.warning')}
             </Alert>
index 9d462d5d5846ed106012c65a422b356615f512bf..18fdae148619338561dc9d756587682ff987bb99 100644 (file)
@@ -19,7 +19,7 @@
  */
 import * as React from 'react';
 import { shallow } from 'enzyme';
-import ChangeVisibilityForm, { Props } from '../ChangeVisibilityForm';
+import ChangeVisibilityForm from '../ChangeVisibilityForm';
 import { click } from '../../../helpers/testUtils';
 import { Visibility } from '../../../app/types';
 
@@ -62,7 +62,7 @@ it('changes visibility', () => {
   expect(onConfirm).toBeCalledWith(Visibility.Private);
 });
 
-function shallowRender(props?: { [P in keyof Props]?: Props[P] }) {
+function shallowRender(props: Partial<ChangeVisibilityForm['props']> = {}) {
   return shallow(
     <ChangeVisibilityForm
       onClose={jest.fn()}
index d7287c5cf66ee6f8dd4c80e02a23b750ddd10cc6..57c9ffb63d0e29f75407a209f45d871386ac205b 100644 (file)
@@ -70,7 +70,9 @@ export default class OrganizationStep extends React.PureComponent<Props, State>
     getOrganizations({ member: true }).then(
       ({ organizations }) => {
         if (this.mounted) {
-          const organizationKeys = organizations.filter(o => o.isAdmin).map(o => o.key);
+          const organizationKeys = organizations
+            .filter(o => o.actions && o.actions.admin)
+            .map(o => o.key);
           // best guess: if there is only one organization, then it is personal
           // otherwise, we can't guess, let's display them all as just "existing organizations"
           const personalOrganization =
index b2192a0bce7186222846e39276b539a3ba4828fa..7bd23503516a681f3ce25f6c49af304f3dbcc6bf 100644 (file)
@@ -26,7 +26,10 @@ import { getOrganizations } from '../../../../api/organizations';
 jest.mock('../../../../api/organizations', () => ({
   getOrganizations: jest.fn(() =>
     Promise.resolve({
-      organizations: [{ isAdmin: true, key: 'user' }, { isAdmin: true, key: 'another' }]
+      organizations: [
+        { actions: { admin: true }, key: 'user' },
+        { actions: { admin: true }, key: 'another' }
+      ]
     })
   )
 }));
index 1a72a92c3548d08d9557d30e295a331db4be9929..dde6fb821a4c149f84f9359b4d4b392be3dd6e57 100644 (file)
@@ -119,16 +119,16 @@ export default connect(mapStateToProps)(PrivacyBadge);
 
 function getDoc(visibility: Visibility, icon: JSX.Element | null, organization: Organization) {
   let doc;
-  const canAdmin = organization.canAdmin || organization.isAdmin;
+  const { actions = {} } = organization;
   if (visibility === Visibility.Private) {
     doc = import(/* webpackMode: "eager" */ 'Docs/tooltips/project/visibility-private.md');
   } else if (icon) {
-    if (canAdmin) {
+    if (actions.admin) {
       doc = import(/* webpackMode: "eager" */ 'Docs/tooltips/project/visibility-public-paid-org-admin.md');
     } else {
       doc = import(/* webpackMode: "eager" */ 'Docs/tooltips/project/visibility-public-paid-org.md');
     }
-  } else if (canAdmin) {
+  } else if (actions.admin) {
     doc = import(/* webpackMode: "eager" */ 'Docs/tooltips/project/visibility-public-admin.md');
   } else {
     doc = import(/* webpackMode: "eager" */ 'Docs/tooltips/project/visibility-public.md');
index 336a20948da026dc8aaef6d530f13db5692af37a..d087ccea800ad7cd65132c15def5382106552ba5 100644 (file)
@@ -47,7 +47,7 @@ it('renders public with icon', () => {
     getWrapper({
       organization: {
         ...organization,
-        canAdmin: true,
+        actions: { admin: true },
         subscription: OrganizationSubscription.Paid
       },
       visibility: Visibility.Public
index a78a573f41cb436afb9421950ddbdd579bf8ab9f..11ab3e1a6c48f5fa8a7f9e4bc32289c75fb9b3c5 100644 (file)
@@ -28,6 +28,7 @@ interface Props {
 }
 
 export default function OrganizationListItem({ organization }: Props) {
+  const { actions = {} } = organization;
   return (
     <li>
       <OrganizationLink className="display-flex-center" organization={organization}>
@@ -35,9 +36,7 @@ export default function OrganizationListItem({ organization }: Props) {
           <OrganizationAvatar organization={organization} small={true} />
           <span className="spacer-left">{organization.name}</span>
         </div>
-        {organization.isAdmin && (
-          <span className="outline-badge spacer-left">{translate('admin')}</span>
-        )}
+        {actions.admin && <span className="outline-badge spacer-left">{translate('admin')}</span>}
       </OrganizationLink>
     </li>
   );
index 6503fcbe5fced9deb1e4f6bc1a34f3d3cb1d9bf0..ed060178aa8c7044f426acb94aa0c8857ba976b8 100644 (file)
@@ -27,7 +27,7 @@ it('renders', () => {
     shallow(
       <OrganizationListItem
         organization={{
-          isAdmin: true,
+          actions: { admin: true },
           key: 'org',
           name: 'org',
           projectVisibility: Visibility.Public
index 9e7c634c9dd42c67fa59892173ae197be29d380a..da344815e73cd38f73bbbc6ecc57a9352dc870ce 100644 (file)
@@ -6,7 +6,9 @@ exports[`renders 1`] = `
     className="display-flex-center"
     organization={
       Object {
-        "isAdmin": true,
+        "actions": Object {
+          "admin": true,
+        },
         "key": "org",
         "name": "org",
         "projectVisibility": "public",
@@ -17,7 +19,9 @@ exports[`renders 1`] = `
       <OrganizationAvatar
         organization={
           Object {
-            "isAdmin": true,
+            "actions": Object {
+              "admin": true,
+            },
             "key": "org",
             "name": "org",
             "projectVisibility": "public",
index a483c995c6cb520e776a7bf03279d6956903e50d..959a9777b69dbfe215d45d8b528da1893a62b848 100644 (file)
@@ -21,7 +21,7 @@ import { hasPrivateAccess, isCurrentUserMemberOf } from '../organizations';
 import { OrganizationSubscription } from '../../app/types';
 
 const org = { key: 'foo', name: 'Foo', subscription: OrganizationSubscription.Paid };
-const adminOrg = { key: 'bar', name: 'Bar', canAdmin: true };
+const adminOrg = { actions: { admin: true }, key: 'bar', name: 'Bar' };
 const randomOrg = { key: 'bar', name: 'Bar' };
 
 const loggedIn = {
index 2c0cdc4f3735dbd9737c1ae07a93041a62d93bac..81852dd8776ca4eee72544cf28736b5c2f12e735 100644 (file)
@@ -43,8 +43,7 @@ export function isCurrentUserMemberOf(
   return Boolean(
     organization &&
       isLoggedIn(currentUser) &&
-      (organization.canAdmin ||
-        organization.isAdmin ||
+      ((organization.actions && organization.actions.admin) ||
         userOrganizations.some(org => org.key === organization.key))
   );
 }
index bc2ba11d2bcfdfcba4661d0d44c09aa5225ef9ca..4ac73b4597fb461b4d4f31830bbe33c25cd270e0 100644 (file)
@@ -4,7 +4,9 @@ exports[`Reducer should create organization 1`] = `
 Object {
   "byKey": Object {
     "foo": Object {
-      "isAdmin": true,
+      "actions": Object {
+        "admin": true,
+      },
       "key": "foo",
       "name": "foo",
     },
@@ -84,8 +86,10 @@ exports[`Reducer should update organization 1`] = `
 Object {
   "byKey": Object {
     "foo": Object {
+      "actions": Object {
+        "admin": true,
+      },
       "description": "description",
-      "isAdmin": true,
       "key": "foo",
       "name": "bar",
     },
index 6b1dfdcc28cc12a81db410632f3992382f3d8467..2eb0b51df2cae8ca2436881392f01ceb0b1586ba 100644 (file)
@@ -71,7 +71,13 @@ function byKey(state: State['byKey'] = {}, action: Action): State['byKey'] {
     case 'RECEIVE_MY_ORGANIZATIONS':
       return onReceiveOrganizations(state, action);
     case 'CREATE_ORGANIZATION':
-      return { ...state, [action.organization.key]: { ...action.organization, isAdmin: true } };
+      return {
+        ...state,
+        [action.organization.key]: {
+          ...action.organization,
+          actions: { ...(action.organization.actions || {}), admin: true }
+        }
+      };
     case 'UPDATE_ORGANIZATION':
       return {
         ...state,
index 1d729266ddc804571e5206b12d0017f6bcb73ffb..2480af5490479f26145cc448b5552b49a74e56d9 100644 (file)
@@ -60,13 +60,19 @@ message Organization {
   optional string url = 4;
   optional string avatar = 5;
   optional bool guarded = 6;
-  optional bool isAdmin = 7;
   optional Alm alm = 8;
+  optional Actions actions = 9;
 
   message Alm {
     optional string key = 1;
     optional string url = 2;
   }
+
+  message Actions {
+    optional bool admin = 1;
+    optional bool delete = 2;
+    optional bool provision = 3;
+  }
 }
 
 message User {