]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-8910 Populate organization members
authorJulien Lancelot <julien.lancelot@sonarsource.com>
Mon, 13 Mar 2017 09:25:38 +0000 (10:25 +0100)
committerJulien Lancelot <julien.lancelot@sonarsource.com>
Tue, 21 Mar 2017 12:05:50 +0000 (13:05 +0100)
server/sonar-db-core/src/main/resources/org/sonar/db/version/rows-h2.sql
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v64/DbVersion64.java
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v64/PopulateOrganizationMembersTable.java [new file with mode: 0644]
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v64/DbVersion64Test.java
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v64/PopulateOrganizationMembersTableTest.java [new file with mode: 0644]
server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v64/PopulateOrganizationMembersTableTest/initial.sql [new file with mode: 0644]

index bdc5d70fd8841cc1ff767ae1e94974cf7af8804d..d9cf14ec43081a83c9edeb66516ea3f99bdae9aa 100644 (file)
@@ -540,6 +540,7 @@ INSERT INTO SCHEMA_MIGRATIONS(VERSION) VALUES ('1604');
 INSERT INTO SCHEMA_MIGRATIONS(VERSION) VALUES ('1605');
 INSERT INTO SCHEMA_MIGRATIONS(VERSION) VALUES ('1606');
 INSERT INTO SCHEMA_MIGRATIONS(VERSION) VALUES ('1607');
+INSERT INTO SCHEMA_MIGRATIONS(VERSION) VALUES ('1608');
 
 INSERT INTO USERS(ID, LOGIN, NAME, EMAIL, EXTERNAL_IDENTITY, EXTERNAL_IDENTITY_PROVIDER, USER_LOCAL, CRYPTED_PASSWORD, SALT, IS_ROOT, CREATED_AT, UPDATED_AT) VALUES (1, 'admin', 'Administrator', '', 'admin', 'sonarqube', true, 'a373a0e667abb2604c1fd571eb4ad47fe8cc0878', '48bc4b0d93179b5103fd3885ea9119498e9d161b', false, '1418215735482', '1418215735482');
 ALTER TABLE USERS ALTER COLUMN ID RESTART WITH 2;
index 41e497da06bc3f0e818f1eed3b5749b448b0d4a1..90693d1b7e61ec44c1c5acda68b33b0100f919d6 100644 (file)
@@ -24,6 +24,7 @@ import org.sonar.server.platform.db.migration.step.MigrationStepRegistry;
 import org.sonar.server.platform.db.migration.version.DbVersion;
 
 public class DbVersion64 implements DbVersion {
+
   @Override
   public void addSteps(MigrationStepRegistry registry) {
     registry
@@ -34,6 +35,7 @@ public class DbVersion64 implements DbVersion {
       .add(1604, "Make RULES_PROFILES.ORGANIZATION_UUID not nullable", MakeQualityProfileOrganizationUuidNotNullable.class)
       .add(1605, "Drop unique index on RULES_PROFILES.KEE", DropUniqueIndexOnQualityProfileKey.class)
       .add(1606, "Make RULES_PROFILES.ORGANIZATION_UUID and KEE unique", MakeQualityProfileOrganizationUuidAndKeyUnique.class)
-      .add(1607, "Create ORGANIZATION_MEMBERS table", CreateOrganizationMembersTable.class);
+      .add(1607, "Create ORGANIZATION_MEMBERS table", CreateOrganizationMembersTable.class)
+      .add(1608, "Populate ORGANIZATION_MEMBERS table", PopulateOrganizationMembersTable.class);
   }
 }
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v64/PopulateOrganizationMembersTable.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v64/PopulateOrganizationMembersTable.java
new file mode 100644 (file)
index 0000000..46df4f3
--- /dev/null
@@ -0,0 +1,83 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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.
+ */
+
+package org.sonar.server.platform.db.migration.version.v64;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.step.DataChange;
+import org.sonar.server.platform.db.migration.step.MassUpdate;
+import org.sonar.server.platform.db.migration.version.v63.DefaultOrganizationUuid;
+
+public class PopulateOrganizationMembersTable extends DataChange {
+
+  private static final String INSERT_ORGANIZATION_MEMBERS_SQL = "INSERT INTO organization_members (user_id, organization_uuid) VALUES (?, ?)";
+
+  private final DefaultOrganizationUuid defaultOrganizationUuid;
+
+  public PopulateOrganizationMembersTable(Database db, DefaultOrganizationUuid defaultOrganizationUuid) {
+    super(db);
+    this.defaultOrganizationUuid = defaultOrganizationUuid;
+  }
+
+  @Override
+  public void execute(Context context) throws SQLException {
+    associateUsersToDefaultOrganization(context);
+    associateUsersToOrganizationBasedOnPermission(context);
+  }
+
+  private void associateUsersToDefaultOrganization(Context context) throws SQLException {
+    String organizationUuid = defaultOrganizationUuid.getAndCheck(context);
+    MassUpdate massUpdate = context.prepareMassUpdate().rowPluralName("default organization members");
+    massUpdate.select(
+      "SELECT u.id FROM users u " +
+        "WHERE u.active=? AND " +
+        "NOT EXISTS (SELECT 1 FROM organization_members om WHERE om.user_id=u.id AND om.organization_uuid=?) ")
+      .setBoolean(1, true)
+      .setString(2, organizationUuid);
+    massUpdate.update(INSERT_ORGANIZATION_MEMBERS_SQL);
+    massUpdate.execute((row, update) -> {
+      update.setInt(1, row.getInt(1));
+      update.setString(2, organizationUuid);
+      return true;
+    });
+  }
+
+  private static void associateUsersToOrganizationBasedOnPermission(Context context) throws SQLException {
+    MassUpdate massUpdate = context.prepareMassUpdate().rowPluralName("non default organization members");
+    massUpdate.select(
+      "SELECT distinct ur.organization_uuid, ur.user_id FROM user_roles ur " +
+        "INNER JOIN users u ON u.id=ur.user_id AND u.active=? " +
+        "WHERE NOT EXISTS (SELECT 1 FROM organization_members om WHERE om.user_id=ur.user_id AND om.organization_uuid=ur.organization_uuid) " +
+        "UNION " +
+        "SELECT distinct g.organization_uuid, gu.user_id FROM groups_users gu " +
+        "INNER JOIN users u ON u.id=gu.user_id AND u.active=? " +
+        "INNER JOIN groups g ON g.id=gu.group_id " +
+        "WHERE NOT EXISTS (SELECT 1 FROM organization_members om WHERE om.user_id=gu.user_id AND om.organization_uuid=g.organization_uuid)")
+      .setBoolean(1, true)
+      .setBoolean(2, true);
+    massUpdate.update(INSERT_ORGANIZATION_MEMBERS_SQL);
+    massUpdate.execute((row, update) -> {
+      update.setInt(1, row.getInt(2));
+      update.setString(2, row.getString(1));
+      return true;
+    });
+  }
+}
index 59e6ba6038edb2ee18f7c22aef571c6c49102af1..9dcd8b2800d4a001aa9bc1dcc2877e1b58db0f4c 100644 (file)
@@ -35,7 +35,7 @@ public class DbVersion64Test {
 
   @Test
   public void verify_migration_count() {
-    verifyMigrationCount(underTest, 8);
+    verifyMigrationCount(underTest, 9);
   }
 
 }
diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v64/PopulateOrganizationMembersTableTest.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v64/PopulateOrganizationMembersTableTest.java
new file mode 100644 (file)
index 0000000..ecb1374
--- /dev/null
@@ -0,0 +1,312 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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.
+ */
+
+package org.sonar.server.platform.db.migration.version.v64;
+
+import com.google.common.collect.ImmutableMap;
+import java.sql.SQLException;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import javax.annotation.Nullable;
+import org.apache.commons.lang.RandomStringUtils;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.core.util.stream.Collectors;
+import org.sonar.db.CoreDbTester;
+import org.sonar.server.platform.db.migration.version.v63.DefaultOrganizationUuidImpl;
+
+import static java.lang.String.format;
+import static org.apache.commons.lang.math.RandomUtils.nextLong;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class PopulateOrganizationMembersTableTest {
+
+  private static final String TABLE = "organization_members";
+
+  private static final String DEFAULT_ORGANIZATION_UUID = "def org uuid";
+
+  private static final String PERMISSION_PROVISIONING = "provisioning";
+  private static final String PERMISSION_ADMIN = "admin";
+  private static final String PERMISSION_BROWSE = "user";
+  private static final String PERMISSION_CODEVIEWER = "codeviewer";
+
+  private static final String ORG1_UUID = "ORG1_UUID";
+  private static final String ORG2_UUID = "ORG2_UUID";
+
+  private static final String USER1_LOGIN = "USER1";
+  private static final String USER2_LOGIN = "USER2";
+  private static final String USER3_LOGIN = "USER3";
+
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  @Rule
+  public CoreDbTester db = CoreDbTester.createForSchema(PopulateOrganizationMembersTableTest.class, "initial.sql");
+
+  private PopulateOrganizationMembersTable underTest = new PopulateOrganizationMembersTable(db.database(), new DefaultOrganizationUuidImpl());
+
+  @Test
+  public void fails_with_ISE_when_no_default_organization_is_set() throws SQLException {
+    expectedException.expect(IllegalStateException.class);
+    expectedException.expectMessage("Default organization uuid is missing");
+
+    underTest.execute();
+  }
+
+  @Test
+  public void fails_with_ISE_when_default_organization_does_not_exist_in_table_ORGANIZATIONS() throws SQLException {
+    setDefaultOrganizationProperty("blabla");
+
+    expectedException.expect(IllegalStateException.class);
+    expectedException.expectMessage("Default organization with uuid 'blabla' does not exist in table ORGANIZATIONS");
+
+    underTest.execute();
+  }
+
+  @Test
+  public void execute_has_no_effect_when_table_is_empty() throws SQLException {
+    setupDefaultOrganization();
+
+    underTest.execute();
+  }
+
+  @Test
+  public void execute_is_reentrant_when_table_is_empty() throws SQLException {
+    setupDefaultOrganization();
+
+    underTest.execute();
+    underTest.execute();
+  }
+
+  @Test
+  public void migrate_user_having_direct_global_permissions() throws Exception {
+    setupDefaultOrganization();
+    insertOrganization(ORG1_UUID);
+    insertOrganization(ORG2_UUID);
+    int userId = insertUser(USER1_LOGIN);
+    insertUserRole(userId, PERMISSION_PROVISIONING, ORG1_UUID, null);
+    insertUserRole(userId, PERMISSION_ADMIN, ORG1_UUID, null);
+    insertUserRole(userId, PERMISSION_ADMIN, ORG2_UUID, null);
+
+    underTest.execute();
+
+    verifyUserMembership(userId, ORG1_UUID, ORG2_UUID, DEFAULT_ORGANIZATION_UUID);
+  }
+
+  @Test
+  public void migrate_user_having_direct_project_permissions() throws Exception {
+    setupDefaultOrganization();
+    insertOrganization(ORG1_UUID);
+    insertOrganization(ORG2_UUID);
+    int userId = insertUser(USER1_LOGIN);
+    insertUserRole(userId, PERMISSION_BROWSE, ORG1_UUID, 1);
+    insertUserRole(userId, PERMISSION_CODEVIEWER, ORG1_UUID, 1);
+    insertUserRole(userId, PERMISSION_ADMIN, ORG2_UUID, 2);
+
+    underTest.execute();
+
+    verifyUserMembership(userId, ORG1_UUID, ORG2_UUID, DEFAULT_ORGANIZATION_UUID);
+  }
+
+  @Test
+  public void migrate_user_having_global_permissions_from_group() throws Exception {
+    setupDefaultOrganization();
+    insertOrganization(ORG1_UUID);
+    insertOrganization(ORG2_UUID);
+    int userId = insertUser(USER1_LOGIN);
+    int group1Id = insertNewGroup(ORG1_UUID);
+    int group2Id = insertNewGroup(ORG2_UUID);
+    insertUserGroup(userId, group1Id);
+    insertUserGroup(userId, group2Id);
+
+    underTest.execute();
+
+    verifyUserMembership(userId, ORG1_UUID, ORG2_UUID, DEFAULT_ORGANIZATION_UUID);
+  }
+
+  @Test
+  public void user_without_any_permission_should_be_member_of_default_organization() throws Exception {
+    setupDefaultOrganization();
+    int userId = insertUser(USER1_LOGIN);
+
+    underTest.execute();
+
+    verifyUserMembership(userId, DEFAULT_ORGANIZATION_UUID, DEFAULT_ORGANIZATION_UUID);
+  }
+
+  @Test
+  public void migrate_users_having_any_kind_of_permission() throws Exception {
+    setupDefaultOrganization();
+    insertOrganization(ORG1_UUID);
+    insertOrganization(ORG2_UUID);
+    int user1 = insertUser(USER1_LOGIN);
+    int user2 = insertUser(USER2_LOGIN);
+    int user3 = insertUser(USER3_LOGIN);
+    int groupId = insertNewGroup(ORG1_UUID);
+    insertUserGroup(user2, groupId);
+    insertUserRole(user1, PERMISSION_PROVISIONING, ORG1_UUID, null);
+    insertUserRole(user1, PERMISSION_BROWSE, ORG2_UUID, 1);
+
+    underTest.execute();
+
+    verifyUserMembership(user1, ORG1_UUID, ORG2_UUID, DEFAULT_ORGANIZATION_UUID);
+    verifyUserMembership(user2, ORG1_UUID, DEFAULT_ORGANIZATION_UUID);
+    verifyUserMembership(user3, DEFAULT_ORGANIZATION_UUID);
+  }
+
+  @Test
+  public void migrate_missing_membership_on_direct_permission() throws Exception {
+    setupDefaultOrganization();
+    insertOrganization(ORG1_UUID);
+    insertOrganization(ORG2_UUID);
+    int userId = insertUser(USER1_LOGIN);
+    insertUserRole(userId, PERMISSION_ADMIN, ORG1_UUID, null);
+    insertUserRole(userId, PERMISSION_PROVISIONING, ORG2_UUID, null);
+    // Membership on organization 1 already exists, migration will add membership on organization 2 and default organization
+    insertOrganizationMember(userId, ORG1_UUID);
+
+    underTest.execute();
+
+    verifyUserMembership(userId, ORG1_UUID, ORG2_UUID, DEFAULT_ORGANIZATION_UUID);
+  }
+
+  @Test
+  public void migrate_missing_membership_on_group_permission() throws Exception {
+    setupDefaultOrganization();
+    insertOrganization(ORG1_UUID);
+    insertOrganization(ORG2_UUID);
+    int userId = insertUser(USER1_LOGIN);
+    int group1Id = insertNewGroup(ORG1_UUID);
+    int group2Id = insertNewGroup(ORG2_UUID);
+    insertUserGroup(userId, group1Id);
+    insertUserGroup(userId, group2Id);
+    // Membership on organization 1 already exists, migration will add membership on organization 2 and default organization
+    insertOrganizationMember(userId, ORG1_UUID);
+
+    underTest.execute();
+
+    verifyUserMembership(userId, ORG1_UUID, ORG2_UUID, DEFAULT_ORGANIZATION_UUID);
+  }
+
+  @Test
+  public void migrate_active_users_to_default_organization() throws Exception {
+    setupDefaultOrganization();
+    int user1Id = insertUser(USER1_LOGIN, false);
+    int user2Id = insertUser(USER2_LOGIN, false);
+    int user3Id = insertUser(USER3_LOGIN, false);
+    int group1Id = insertNewGroup(ORG1_UUID);
+    insertUserRole(user1Id, PERMISSION_ADMIN, ORG1_UUID, null);
+    insertUserGroup(user2Id, group1Id);
+
+    underTest.execute();
+
+    verifyUserMembership(user1Id);
+    verifyUserMembership(user2Id);
+    verifyUserMembership(user3Id);
+  }
+
+  @Test
+  public void ignore_already_associated_users() throws Exception {
+    setupDefaultOrganization();
+    insertOrganization(ORG1_UUID);
+    int userId = insertUser(USER1_LOGIN);
+    insertUserRole(userId, PERMISSION_PROVISIONING, ORG1_UUID, null);
+    // User is already associated to organization 1 and to default organization, it should not fail
+    insertOrganizationMember(userId, ORG1_UUID);
+    insertOrganizationMember(userId, DEFAULT_ORGANIZATION_UUID);
+
+    underTest.execute();
+
+    verifyUserMembership(userId, ORG1_UUID, DEFAULT_ORGANIZATION_UUID);
+  }
+
+  @Test
+  public void migration_is_reentrant() throws Exception {
+    setupDefaultOrganization();
+    insertOrganization(ORG1_UUID);
+    int userId = insertUser(USER1_LOGIN);
+    insertUserRole(userId, PERMISSION_PROVISIONING, ORG1_UUID, null);
+    verifyUserMembership(userId);
+
+    underTest.execute();
+    verifyUserMembership(userId, ORG1_UUID, DEFAULT_ORGANIZATION_UUID);
+
+    underTest.execute();
+    verifyUserMembership(userId, ORG1_UUID, DEFAULT_ORGANIZATION_UUID);
+  }
+
+  private void insertOrganizationMember(int userId, String organizationUuid) {
+    db.executeInsert(TABLE, "USER_ID", userId, "ORGANIZATION_UUID", organizationUuid);
+  }
+
+  private void insertOrganization(String uuid) {
+    db.executeInsert("ORGANIZATIONS", "UUID", uuid, "KEE", uuid, "NAME", uuid, "GUARDED", false, "CREATED_AT", nextLong(), "UPDATED_AT", nextLong());
+  }
+
+  private int insertUser(String login) {
+    return insertUser(login, true);
+  }
+
+  private int insertUser(String login, boolean enabled) {
+    db.executeInsert("USERS", "LOGIN", login, "NAME", login, "ACTIVE", enabled, "IS_ROOT", false);
+    return ((Long) db.selectFirst(format("select ID from users where login='%s'", login)).get("ID")).intValue();
+  }
+
+  private void insertUserRole(int userId, String permission, String organizationUuid, @Nullable Integer componentId) {
+    ImmutableMap.Builder<String, Object> builder = ImmutableMap.<String, Object>builder()
+      .putAll(ImmutableMap.of("USER_ID", userId, "ROLE", permission, "ORGANIZATION_UUID", organizationUuid));
+    Optional.ofNullable(componentId).ifPresent(id -> builder.put("RESOURCE_ID", id));
+    db.executeInsert("USER_ROLES", builder.build());
+  }
+
+  private int insertNewGroup(String organizationUuid) {
+    String groupName = RandomStringUtils.random(10);
+    db.executeInsert("GROUPS", "NAME", groupName, "ORGANIZATION_UUID", organizationUuid);
+    return ((Long) db.selectFirst(format("select ID from groups where name='%s' and organization_uuid='%s'", groupName, organizationUuid)).get("ID")).intValue();
+  }
+
+  private void insertUserGroup(int userId, int groupId) {
+    db.executeInsert("GROUPS_USERS", "USER_ID", userId, "GROUP_ID", groupId);
+  }
+
+  private void setupDefaultOrganization() {
+    setDefaultOrganizationProperty(DEFAULT_ORGANIZATION_UUID);
+    insertOrganization(DEFAULT_ORGANIZATION_UUID);
+  }
+
+  private void setDefaultOrganizationProperty(String defaultOrganizationUuid) {
+    db.executeInsert(
+      "INTERNAL_PROPERTIES",
+      "KEE", "organization.default",
+      "IS_EMPTY", "false",
+      "TEXT_VALUE", defaultOrganizationUuid);
+  }
+
+  private void verifyUserMembership(int userId, String... organizationUuids) {
+    List<Map<String, Object>> rows = db.select(format("SELECT ORGANIZATION_UUID FROM " + TABLE + " WHERE USER_ID = %s", userId));
+    List<String> userOrganizationUuids = rows.stream()
+      .map(values -> (String) values.get("ORGANIZATION_UUID"))
+      .collect(Collectors.toList());
+    assertThat(userOrganizationUuids).containsOnly(organizationUuids);
+  }
+
+}
diff --git a/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v64/PopulateOrganizationMembersTableTest/initial.sql b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v64/PopulateOrganizationMembersTableTest/initial.sql
new file mode 100644 (file)
index 0000000..6b92f40
--- /dev/null
@@ -0,0 +1,77 @@
+CREATE TABLE "ORGANIZATION_MEMBERS" (
+  "ORGANIZATION_UUID" VARCHAR(40) NOT NULL,
+  "USER_ID" INTEGER NOT NULL
+);
+CREATE PRIMARY KEY ON "ORGANIZATION_MEMBERS" ("ORGANIZATION_UUID", "USER_ID");
+
+CREATE TABLE "ORGANIZATIONS" (
+  "UUID" VARCHAR(40) NOT NULL PRIMARY KEY,
+  "KEE" VARCHAR(32) NOT NULL,
+  "NAME" VARCHAR(64) NOT NULL,
+  "DESCRIPTION" VARCHAR(256),
+  "URL" VARCHAR(256),
+  "AVATAR_URL" VARCHAR(256),
+  "GUARDED" BOOLEAN NOT NULL,
+  "USER_ID" INTEGER,
+  "DEFAULT_PERM_TEMPLATE_PROJECT" VARCHAR(40),
+  "DEFAULT_PERM_TEMPLATE_VIEW" VARCHAR(40),
+  "CREATED_AT" BIGINT NOT NULL,
+  "UPDATED_AT" BIGINT NOT NULL
+);
+CREATE UNIQUE INDEX "PK_ORGANIZATIONS" ON "ORGANIZATIONS" ("UUID");
+CREATE UNIQUE INDEX "ORGANIZATION_KEY" ON "ORGANIZATIONS" ("KEE");
+
+CREATE TABLE "INTERNAL_PROPERTIES" (
+  "KEE" VARCHAR(50) NOT NULL PRIMARY KEY,
+  "IS_EMPTY" BOOLEAN NOT NULL,
+  "TEXT_VALUE" VARCHAR(4000),
+  "CLOB_VALUE" CLOB,
+  "CREATED_AT" BIGINT
+);
+CREATE UNIQUE INDEX "UNIQ_INTERNAL_PROPERTIES" ON "INTERNAL_PROPERTIES" ("KEE");
+
+CREATE TABLE "USERS" (
+  "ID" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1),
+  "LOGIN" VARCHAR(255),
+  "NAME" VARCHAR(200),
+  "EMAIL" VARCHAR(100),
+  "CRYPTED_PASSWORD" VARCHAR(40),
+  "SALT" VARCHAR(40),
+  "ACTIVE" BOOLEAN DEFAULT TRUE,
+  "SCM_ACCOUNTS" VARCHAR(4000),
+  "EXTERNAL_IDENTITY" VARCHAR(255),
+  "EXTERNAL_IDENTITY_PROVIDER" VARCHAR(100),
+  "IS_ROOT" BOOLEAN NOT NULL,
+  "USER_LOCAL" BOOLEAN,
+  "CREATED_AT" BIGINT,
+  "UPDATED_AT" BIGINT
+);
+CREATE UNIQUE INDEX "USERS_LOGIN" ON "USERS" ("LOGIN");
+CREATE INDEX "USERS_UPDATED_AT" ON "USERS" ("UPDATED_AT");
+
+CREATE TABLE "GROUPS_USERS" (
+  "USER_ID" INTEGER,
+  "GROUP_ID" INTEGER
+);
+CREATE INDEX "INDEX_GROUPS_USERS_ON_GROUP_ID" ON "GROUPS_USERS" ("GROUP_ID");
+CREATE INDEX "INDEX_GROUPS_USERS_ON_USER_ID" ON "GROUPS_USERS" ("USER_ID");
+CREATE UNIQUE INDEX "GROUPS_USERS_UNIQUE" ON "GROUPS_USERS" ("GROUP_ID", "USER_ID");
+
+CREATE TABLE "USER_ROLES" (
+  "ID" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1),
+  "ORGANIZATION_UUID" VARCHAR(40) NOT NULL,
+  "USER_ID" INTEGER,
+  "RESOURCE_ID" INTEGER,
+  "ROLE" VARCHAR(64) NOT NULL
+);
+CREATE INDEX "USER_ROLES_RESOURCE" ON "USER_ROLES" ("RESOURCE_ID");
+CREATE INDEX "USER_ROLES_USER" ON "USER_ROLES" ("USER_ID");
+
+CREATE TABLE "GROUPS" (
+  "ID" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1),
+  "ORGANIZATION_UUID" VARCHAR(40) NOT NULL,
+  "NAME" VARCHAR(500),
+  "DESCRIPTION" VARCHAR(200),
+  "CREATED_AT" TIMESTAMP,
+  "UPDATED_AT" TIMESTAMP
+);