From: Julien Lancelot Date: Mon, 13 Mar 2017 09:25:38 +0000 (+0100) Subject: SONAR-8910 Populate organization members X-Git-Tag: 6.4-RC1~732 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=6e544a8a6efd543bab52829392659d2522699a3e;p=sonarqube.git SONAR-8910 Populate organization members --- diff --git a/server/sonar-db-core/src/main/resources/org/sonar/db/version/rows-h2.sql b/server/sonar-db-core/src/main/resources/org/sonar/db/version/rows-h2.sql index bdc5d70fd88..d9cf14ec430 100644 --- a/server/sonar-db-core/src/main/resources/org/sonar/db/version/rows-h2.sql +++ b/server/sonar-db-core/src/main/resources/org/sonar/db/version/rows-h2.sql @@ -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; diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v64/DbVersion64.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v64/DbVersion64.java index 41e497da06b..90693d1b7e6 100644 --- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v64/DbVersion64.java +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v64/DbVersion64.java @@ -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 index 00000000000..46df4f3d8a7 --- /dev/null +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v64/PopulateOrganizationMembersTable.java @@ -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; + }); + } +} diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v64/DbVersion64Test.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v64/DbVersion64Test.java index 59e6ba6038e..9dcd8b2800d 100644 --- a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v64/DbVersion64Test.java +++ b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v64/DbVersion64Test.java @@ -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 index 00000000000..ecb13741caf --- /dev/null +++ b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v64/PopulateOrganizationMembersTableTest.java @@ -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 builder = ImmutableMap.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> rows = db.select(format("SELECT ORGANIZATION_UUID FROM " + TABLE + " WHERE USER_ID = %s", userId)); + List 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 index 00000000000..6b92f403982 --- /dev/null +++ b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v64/PopulateOrganizationMembersTableTest/initial.sql @@ -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 +);