Standalone - Add UserRole table
authorcolemanw <coleman@civicrm.org>
Sat, 9 Dec 2023 16:05:30 +0000 (11:05 -0500)
committercolemanw <coleman@civicrm.org>
Mon, 11 Dec 2023 18:33:37 +0000 (13:33 -0500)
Adds bridge table to connect users with roles,
which makes joins and complex queries possible.
For ease-of-use this re-adds the civicrm_uf_match.roles field as a virtual calculated field.

ext/standaloneusers/CRM/Standaloneusers/BAO/Role.php
ext/standaloneusers/CRM/Standaloneusers/BAO/User.php
ext/standaloneusers/CRM/Standaloneusers/DAO/User.php
ext/standaloneusers/CRM/Standaloneusers/DAO/UserRole.php [new file with mode: 0644]
ext/standaloneusers/Civi/Api4/Service/Spec/Provider/UserSpecProvider.php
ext/standaloneusers/Civi/Api4/UserRole.php [new file with mode: 0644]
ext/standaloneusers/sql/auto_install.sql
ext/standaloneusers/sql/auto_uninstall.sql
ext/standaloneusers/xml/schema/CRM/Standaloneusers/User.xml
ext/standaloneusers/xml/schema/CRM/Standaloneusers/UserRole.entityType.php [new file with mode: 0644]
ext/standaloneusers/xml/schema/CRM/Standaloneusers/UserRole.xml [new file with mode: 0644]

index e9e3ceef8c8a279dbd59a9053e3e291050de7ca6..95af217adb6d2321fe2c9ef849237ecf4846f9bb 100644 (file)
@@ -1,7 +1,10 @@
 <?php
-// phpcs:disable
+/**
+ * @package CRM
+ * @copyright CiviCRM LLC https://civicrm.org/licensing
+ */
+
 use CRM_Standaloneusers_ExtensionUtil as E;
-// phpcs:enable
 
 class CRM_Standaloneusers_BAO_Role extends CRM_Standaloneusers_DAO_Role implements \Civi\Core\HookInterface {
 
@@ -10,21 +13,6 @@ class CRM_Standaloneusers_BAO_Role extends CRM_Standaloneusers_DAO_Role implemen
    * @param \Civi\Core\Event\PostEvent $event
    */
   public static function self_hook_civicrm_post(\Civi\Core\Event\PostEvent $event) {
-    // Remove role from users on deletion
-    if ($event->action === 'delete') {
-      $users = \Civi\Api4\User::get(FALSE)
-        ->addSelect('id', 'roles')
-        ->addWhere('roles', 'CONTAINS', $event->id)
-        ->execute();
-      foreach ($users as $user) {
-        $roles = array_diff($user['roles'], [$event->id]);
-        \Civi\Api4\User::update(FALSE)
-          ->addValue('roles', $roles)
-          ->addWhere('id', '=', $user['id'])
-          ->execute();
-      }
-    }
-
     // Reset cache
     Civi::cache('metadata')->clear();
   }
index bfbb09a837b199ee08e19340b9dbdc03785b3153..d1bbdcb9c44546187efe2a0a967b43e2685ad18f 100644 (file)
@@ -1,27 +1,59 @@
 <?php
-
 /**
  * @package CRM
  * @copyright CiviCRM LLC https://civicrm.org/licensing
  */
 
+use Civi\Api4\UserRole;
+use CRM_Standaloneusers_ExtensionUtil as E;
+
 /**
  * Business access object for the User entity.
  */
 class CRM_Standaloneusers_BAO_User extends CRM_Standaloneusers_DAO_User implements \Civi\Core\HookInterface {
 
   /**
-   * Event fired after an action is taken on a User record.
+   * Event fired before an action is taken on a User record.
    * @param \Civi\Core\Event\PreEvent $event
    */
   public static function self_hook_civicrm_pre(\Civi\Core\Event\PreEvent $event) {
-    if (in_array($event->action, ['create', 'edit'])
-        && empty($event->params['when_updated'])) {
+    if (
+      in_array($event->action, ['create', 'edit'], TRUE) &&
+      empty($event->params['when_updated'])
+    ) {
       // Track when_updated.
       $event->params['when_updated'] = date('YmdHis');
     }
   }
 
+  /**
+   * Event fired after an action is taken on a User record.
+   * @param \Civi\Core\Event\PostEvent $event
+   */
+  public static function self_hook_civicrm_post(\Civi\Core\Event\PostEvent $event) {
+    // Handle virtual "roles" field (defined in UserSpecProvider)
+    // @see \Civi\Api4\Service\Spec\Provider\UserSpecProvider
+    if (
+      in_array($event->action, ['create', 'edit'], TRUE) &&
+      isset($event->params['roles']) && $event->id
+    ) {
+      if ($event->params['roles']) {
+        $newRoles = array_map(function($role_id) {
+          return ['role_id' => $role_id];
+        }, $event->params['roles']);
+        UserRole::replace(FALSE)
+          ->addWhere('user_id', '=', $event->id)
+          ->setRecords($newRoles)
+          ->execute();
+      }
+      else {
+        UserRole::delete(FALSE)
+          ->addWhere('user_id', '=', $event->id)
+          ->execute();
+      }
+    }
+  }
+
   public static function updateLastAccessed() {
     $sess = CRM_Core_Session::singleton();
     $ufID = (int) $sess->get('ufID');
index 4cc9d4b60846362341df708b081ef053df84507d..67331d4a2ec52bc3099c1b37b5833c218a5d634e 100644 (file)
@@ -6,7 +6,7 @@
  *
  * Generated from standaloneusers/xml/schema/CRM/Standaloneusers/User.xml
  * DO NOT EDIT.  Generated by CRM_Core_CodeGen
- * (GenCodeChecksum:889ac5b24fb6913d046bd2e52dcb65ea)
+ * (GenCodeChecksum:29abb6899d43b942155a37ffeddaa10d)
  */
 use CRM_Standaloneusers_ExtensionUtil as E;
 
@@ -109,15 +109,6 @@ class CRM_Standaloneusers_DAO_User extends CRM_Core_DAO {
    */
   public $hashed_password;
 
-  /**
-   * FK to Role
-   *
-   * @var string|null
-   *   (SQL type: varchar(128))
-   *   Note that values will be retrieved from the database as a string.
-   */
-  public $roles;
-
   /**
    * @var string|null
    *   (SQL type: timestamp)
@@ -377,37 +368,6 @@ class CRM_Standaloneusers_DAO_User extends CRM_Core_DAO {
           'readonly' => TRUE,
           'add' => NULL,
         ],
-        'roles' => [
-          'name' => 'roles',
-          'type' => CRM_Utils_Type::T_STRING,
-          'title' => E::ts('Roles'),
-          'description' => E::ts('FK to Role'),
-          'maxlength' => 128,
-          'size' => CRM_Utils_Type::HUGE,
-          'usage' => [
-            'import' => FALSE,
-            'export' => FALSE,
-            'duplicate_matching' => FALSE,
-            'token' => FALSE,
-          ],
-          'where' => 'civicrm_uf_match.roles',
-          'table_name' => 'civicrm_uf_match',
-          'entity' => 'User',
-          'bao' => 'CRM_Standaloneusers_DAO_User',
-          'localizable' => 0,
-          'serialize' => self::SERIALIZE_SEPARATOR_BOOKEND,
-          'html' => [
-            'type' => 'Select',
-          ],
-          'pseudoconstant' => [
-            'table' => 'civicrm_role',
-            'keyColumn' => 'id',
-            'labelColumn' => 'label',
-            'nameColumn' => 'name',
-            'condition' => 'name != "everyone"',
-          ],
-          'add' => NULL,
-        ],
         'when_created' => [
           'name' => 'when_created',
           'type' => CRM_Utils_Type::T_TIMESTAMP,
diff --git a/ext/standaloneusers/CRM/Standaloneusers/DAO/UserRole.php b/ext/standaloneusers/CRM/Standaloneusers/DAO/UserRole.php
new file mode 100644 (file)
index 0000000..4544b45
--- /dev/null
@@ -0,0 +1,245 @@
+<?php
+
+/**
+ * @package CRM
+ * @copyright CiviCRM LLC https://civicrm.org/licensing
+ *
+ * Generated from standaloneusers/xml/schema/CRM/Standaloneusers/UserRole.xml
+ * DO NOT EDIT.  Generated by CRM_Core_CodeGen
+ * (GenCodeChecksum:261a62783e6628b718b812b096080e69)
+ */
+use CRM_Standaloneusers_ExtensionUtil as E;
+
+/**
+ * Database access object for the UserRole entity.
+ */
+class CRM_Standaloneusers_DAO_UserRole extends CRM_Core_DAO {
+  const EXT = E::LONG_NAME;
+  const TABLE_ADDED = '';
+
+  /**
+   * Static instance to hold the table name.
+   *
+   * @var string
+   */
+  public static $_tableName = 'civicrm_user_role';
+
+  /**
+   * Should CiviCRM log any modifications to this table in the civicrm_log table.
+   *
+   * @var bool
+   */
+  public static $_log = TRUE;
+
+  /**
+   * Unique UserRole ID
+   *
+   * @var int|string|null
+   *   (SQL type: int unsigned)
+   *   Note that values will be retrieved from the database as a string.
+   */
+  public $id;
+
+  /**
+   * FK to User
+   *
+   * @var int|string|null
+   *   (SQL type: int unsigned)
+   *   Note that values will be retrieved from the database as a string.
+   */
+  public $user_id;
+
+  /**
+   * FK to Role
+   *
+   * @var int|string|null
+   *   (SQL type: int unsigned)
+   *   Note that values will be retrieved from the database as a string.
+   */
+  public $role_id;
+
+  /**
+   * Class constructor.
+   */
+  public function __construct() {
+    $this->__table = 'civicrm_user_role';
+    parent::__construct();
+  }
+
+  /**
+   * Returns localized title of this entity.
+   *
+   * @param bool $plural
+   *   Whether to return the plural version of the title.
+   */
+  public static function getEntityTitle($plural = FALSE) {
+    return $plural ? E::ts('User Roles') : E::ts('User Role');
+  }
+
+  /**
+   * Returns foreign keys and entity references.
+   *
+   * @return array
+   *   [CRM_Core_Reference_Interface]
+   */
+  public static function getReferenceColumns() {
+    if (!isset(Civi::$statics[__CLASS__]['links'])) {
+      Civi::$statics[__CLASS__]['links'] = static::createReferenceColumns(__CLASS__);
+      Civi::$statics[__CLASS__]['links'][] = new CRM_Core_Reference_Basic(self::getTableName(), 'user_id', 'civicrm_uf_match', 'id');
+      Civi::$statics[__CLASS__]['links'][] = new CRM_Core_Reference_Basic(self::getTableName(), 'role_id', 'civicrm_role', 'id');
+      CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'links_callback', Civi::$statics[__CLASS__]['links']);
+    }
+    return Civi::$statics[__CLASS__]['links'];
+  }
+
+  /**
+   * Returns all the column names of this table
+   *
+   * @return array
+   */
+  public static function &fields() {
+    if (!isset(Civi::$statics[__CLASS__]['fields'])) {
+      Civi::$statics[__CLASS__]['fields'] = [
+        'id' => [
+          'name' => 'id',
+          'type' => CRM_Utils_Type::T_INT,
+          'title' => E::ts('ID'),
+          'description' => E::ts('Unique UserRole ID'),
+          'required' => TRUE,
+          'usage' => [
+            'import' => FALSE,
+            'export' => FALSE,
+            'duplicate_matching' => FALSE,
+            'token' => FALSE,
+          ],
+          'where' => 'civicrm_user_role.id',
+          'table_name' => 'civicrm_user_role',
+          'entity' => 'UserRole',
+          'bao' => 'CRM_Standaloneusers_DAO_UserRole',
+          'localizable' => 0,
+          'html' => [
+            'type' => 'Number',
+          ],
+          'readonly' => TRUE,
+          'add' => NULL,
+        ],
+        'user_id' => [
+          'name' => 'user_id',
+          'type' => CRM_Utils_Type::T_INT,
+          'title' => E::ts('User ID'),
+          'description' => E::ts('FK to User'),
+          'usage' => [
+            'import' => FALSE,
+            'export' => FALSE,
+            'duplicate_matching' => FALSE,
+            'token' => FALSE,
+          ],
+          'where' => 'civicrm_user_role.user_id',
+          'table_name' => 'civicrm_user_role',
+          'entity' => 'UserRole',
+          'bao' => 'CRM_Standaloneusers_DAO_UserRole',
+          'localizable' => 0,
+          'FKClassName' => 'CRM_Standaloneusers_DAO_User',
+          'html' => [
+            'type' => 'EntityRef',
+            'label' => E::ts("User"),
+          ],
+          'add' => NULL,
+        ],
+        'role_id' => [
+          'name' => 'role_id',
+          'type' => CRM_Utils_Type::T_INT,
+          'title' => E::ts('Role ID'),
+          'description' => E::ts('FK to Role'),
+          'usage' => [
+            'import' => FALSE,
+            'export' => FALSE,
+            'duplicate_matching' => FALSE,
+            'token' => FALSE,
+          ],
+          'where' => 'civicrm_user_role.role_id',
+          'table_name' => 'civicrm_user_role',
+          'entity' => 'UserRole',
+          'bao' => 'CRM_Standaloneusers_DAO_UserRole',
+          'localizable' => 0,
+          'FKClassName' => 'CRM_Standaloneusers_DAO_Role',
+          'html' => [
+            'type' => 'EntityRef',
+            'label' => E::ts("Role"),
+          ],
+          'add' => NULL,
+        ],
+      ];
+      CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'fields_callback', Civi::$statics[__CLASS__]['fields']);
+    }
+    return Civi::$statics[__CLASS__]['fields'];
+  }
+
+  /**
+   * Return a mapping from field-name to the corresponding key (as used in fields()).
+   *
+   * @return array
+   *   Array(string $name => string $uniqueName).
+   */
+  public static function &fieldKeys() {
+    if (!isset(Civi::$statics[__CLASS__]['fieldKeys'])) {
+      Civi::$statics[__CLASS__]['fieldKeys'] = array_flip(CRM_Utils_Array::collect('name', self::fields()));
+    }
+    return Civi::$statics[__CLASS__]['fieldKeys'];
+  }
+
+  /**
+   * Returns the names of this table
+   *
+   * @return string
+   */
+  public static function getTableName() {
+    return self::$_tableName;
+  }
+
+  /**
+   * Returns if this table needs to be logged
+   *
+   * @return bool
+   */
+  public function getLog() {
+    return self::$_log;
+  }
+
+  /**
+   * Returns the list of fields that can be imported
+   *
+   * @param bool $prefix
+   *
+   * @return array
+   */
+  public static function &import($prefix = FALSE) {
+    $r = CRM_Core_DAO_AllCoreTables::getImports(__CLASS__, 'user_role', $prefix, []);
+    return $r;
+  }
+
+  /**
+   * Returns the list of fields that can be exported
+   *
+   * @param bool $prefix
+   *
+   * @return array
+   */
+  public static function &export($prefix = FALSE) {
+    $r = CRM_Core_DAO_AllCoreTables::getExports(__CLASS__, 'user_role', $prefix, []);
+    return $r;
+  }
+
+  /**
+   * Returns the list of indices
+   *
+   * @param bool $localize
+   *
+   * @return array
+   */
+  public static function indices($localize = TRUE) {
+    $indices = [];
+    return ($localize && !empty($indices)) ? CRM_Core_DAO_AllCoreTables::multilingualize(__CLASS__, $indices) : $indices;
+  }
+
+}
index 486fa97f871ec198ade27546eb7e4733d2a799ff..66bb99fc7701cb25a843b9a46873e1a96112d82e 100644 (file)
@@ -1,5 +1,4 @@
 <?php
-
 /*
  +--------------------------------------------------------------------+
  | Copyright CiviCRM LLC. All rights reserved.                        |
@@ -12,6 +11,8 @@
 
 namespace Civi\Api4\Service\Spec\Provider;
 
+use CRM_Standaloneusers_ExtensionUtil as E;
+use Civi\Api4\Query\Api4SelectQuery;
 use Civi\Api4\Service\Spec\FieldSpec;
 use Civi\Api4\Service\Spec\RequestSpec;
 
@@ -25,18 +26,53 @@ class UserSpecProvider extends \Civi\Core\Service\AutoService implements Generic
    * @inheritDoc
    */
   public function modifySpec(RequestSpec $spec) {
-    $password = new FieldSpec('password', 'User', 'String');
-    $password->setTitle(ts('New password'));
-    $password->setDescription('Provide a new password for this user.');
-    $password->setInputType('Password');
-    $spec->addFieldSpec($password);
+    // Write-only `password` field
+    if (in_array($spec->getAction(), ['create', 'update', 'save'], TRUE)) {
+      $password = new FieldSpec('password', 'User', 'String');
+      $password->setTitle(E::ts('New password'));
+      $password->setDescription(E::ts('Provide a new password for this user.'));
+      $password->setInputType('Password');
+      $spec->addFieldSpec($password);
+    }
+    // Virtual "roles" field is a facade to the FK values in `civicrm_user_role`.
+    // It makes forms easier to write by acting as if user.roles were a simple field on the User record.
+    $roles = new FieldSpec('roles', 'User', 'Array');
+    $roles->setTitle(E::ts('Roles'));
+    $roles->setDescription(E::ts('Role ids belonging to this user.'));
+    $roles->setInputType('Select');
+    $roles->setInputAttrs(['multiple' => TRUE]);
+    $roles->setSerialize(\CRM_Core_DAO::SERIALIZE_COMMA);
+    $roles->setSuffixes(['id', 'name', 'label']);
+    $roles->setOptionsCallback([__CLASS__, 'getRolesOptions']);
+    $roles->setColumnName('id');
+    $roles->setSqlRenderer([__CLASS__, 'getRolesSql']);
+    $spec->addFieldSpec($roles);
+  }
+
+  public static function getRolesOptions(): array {
+    $roles = \Civi::cache('metadata')->get('user_roles');
+    if (!$roles) {
+      $select = \CRM_Utils_SQL_Select::from('civicrm_role')
+        ->select(['id', 'name', 'label'])
+        ->where('is_active = 1')
+        ->where('name != "everyone"')
+        ->orderBy('label')
+        ->toSQL();
+      $roles = \CRM_Core_DAO::executeQuery($select)->fetchAll();
+      \Civi::cache('metadata')->set('user_roles', $roles);
+    }
+    return $roles;
+  }
+
+  public static function getRolesSql(array $field, Api4SelectQuery $query): string {
+    return "(SELECT GROUP_CONCAT(ur.role_id) FROM civicrm_user_role ur WHERE ur.user_id = {$field['sql_name']})";
   }
 
   /**
    * @inheritDoc
    */
   public function applies($entity, $action) {
-    return $entity === 'User' && in_array($action, ['create', 'update', 'save']);
+    return $entity === 'User';
   }
 
 }
diff --git a/ext/standaloneusers/Civi/Api4/UserRole.php b/ext/standaloneusers/Civi/Api4/UserRole.php
new file mode 100644 (file)
index 0000000..7517955
--- /dev/null
@@ -0,0 +1,15 @@
+<?php
+namespace Civi\Api4;
+
+/**
+ * UserRole entity: links Users with their Role(s).
+ *
+ * Provided by the Standalone Users extension.
+ *
+ * @searchable bridge
+ * @package Civi\Api4
+ */
+class UserRole extends Generic\DAOEntity {
+  use \Civi\Api4\Generic\Traits\EntityBridge;
+
+}
index 9a482bb76e37c6923844f6b396d6b0e4ca34b3d8..c365aad57adb4d01f9abd55c4025586d9afe76c7 100644 (file)
@@ -17,6 +17,7 @@
 
 SET FOREIGN_KEY_CHECKS=0;
 
+DROP TABLE IF EXISTS `civicrm_user_role`;
 DROP TABLE IF EXISTS `civicrm_uf_match`;
 DROP TABLE IF EXISTS `civicrm_role`;
 
@@ -59,7 +60,6 @@ CREATE TABLE `civicrm_uf_match` (
   `contact_id` int unsigned COMMENT 'FK to Contact ID',
   `username` varchar(60) NOT NULL,
   `hashed_password` varchar(128) NOT NULL DEFAULT "" COMMENT 'Hashed, not plaintext password',
-  `roles` varchar(128) COMMENT 'FK to Role',
   `when_created` timestamp DEFAULT CURRENT_TIMESTAMP,
   `when_last_accessed` timestamp NULL,
   `when_updated` timestamp NULL,
@@ -76,3 +76,20 @@ CREATE TABLE `civicrm_uf_match` (
   CONSTRAINT FK_civicrm_uf_match_contact_id FOREIGN KEY (`contact_id`) REFERENCES `civicrm_contact`(`id`) ON DELETE SET NULL
 )
 ENGINE=InnoDB;
+
+-- /*******************************************************
+-- *
+-- * civicrm_user_role
+-- *
+-- * Bridge between users and roles
+-- *
+-- *******************************************************/
+CREATE TABLE `civicrm_user_role` (
+  `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT 'Unique UserRole ID',
+  `user_id` int unsigned COMMENT 'FK to User',
+  `role_id` int unsigned COMMENT 'FK to Role',
+  PRIMARY KEY (`id`),
+  CONSTRAINT FK_civicrm_user_role_user_id FOREIGN KEY (`user_id`) REFERENCES `civicrm_uf_match`(`id`) ON DELETE CASCADE,
+  CONSTRAINT FK_civicrm_user_role_role_id FOREIGN KEY (`role_id`) REFERENCES `civicrm_role`(`id`) ON DELETE CASCADE
+)
+ENGINE=InnoDB;
index 35c0c090b4cf90af67f3bf04db4e1a9f244d459c..8e2ac55abc72dd42b109c0a51bcf54ebb74a751d 100644 (file)
@@ -17,6 +17,7 @@
 
 SET FOREIGN_KEY_CHECKS=0;
 
+DROP TABLE IF EXISTS `civicrm_user_role`;
 DROP TABLE IF EXISTS `civicrm_uf_match`;
 DROP TABLE IF EXISTS `civicrm_role`;
 
index 1fc8cbcb35be823c5101d46d5c4460488f3453f8..9e704f89ee2cc5437ee2f15fee14f2580a257d52 100644 (file)
     <comment>Hashed, not plaintext password</comment>
   </field>
 
-  <field>
-    <name>roles</name>
-    <type>varchar</type>
-    <title>Roles</title>
-    <length>128</length>
-    <comment>FK to Role</comment>
-    <pseudoconstant>
-      <table>civicrm_role</table>
-      <keyColumn>id</keyColumn>
-      <labelColumn>label</labelColumn>
-      <nameColumn>name</nameColumn>
-      <condition>name != "everyone"</condition>
-    </pseudoconstant>
-    <html>
-      <type>Select</type>
-    </html>
-    <serialize>SEPARATOR_BOOKEND</serialize>
-  </field>
-
   <field>
     <name>when_created</name>
     <type>timestamp</type>
diff --git a/ext/standaloneusers/xml/schema/CRM/Standaloneusers/UserRole.entityType.php b/ext/standaloneusers/xml/schema/CRM/Standaloneusers/UserRole.entityType.php
new file mode 100644 (file)
index 0000000..3ef6404
--- /dev/null
@@ -0,0 +1,10 @@
+<?php
+// This file declares a new entity type. For more details, see "hook_civicrm_entityTypes" at:
+// https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_entityTypes
+return [
+  [
+    'name' => 'UserRole',
+    'class' => 'CRM_Standaloneusers_DAO_UserRole',
+    'table' => 'civicrm_user_role',
+  ],
+];
diff --git a/ext/standaloneusers/xml/schema/CRM/Standaloneusers/UserRole.xml b/ext/standaloneusers/xml/schema/CRM/Standaloneusers/UserRole.xml
new file mode 100644 (file)
index 0000000..14cad17
--- /dev/null
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="iso-8859-1" ?>
+
+<table>
+  <base>CRM/Standaloneusers</base>
+  <class>UserRole</class>
+  <name>civicrm_user_role</name>
+  <comment>Bridge between users and roles</comment>
+  <log>true</log>
+
+  <field>
+    <name>id</name>
+    <type>int unsigned</type>
+    <required>true</required>
+    <comment>Unique UserRole ID</comment>
+    <html>
+      <type>Number</type>
+    </html>
+  </field>
+  <primaryKey>
+    <name>id</name>
+    <autoincrement>true</autoincrement>
+  </primaryKey>
+
+  <field>
+    <name>user_id</name>
+    <type>int unsigned</type>
+    <comment>FK to User</comment>
+    <html>
+      <label>User</label>
+      <type>EntityRef</type>
+    </html>
+  </field>
+  <foreignKey>
+    <name>user_id</name>
+    <table>civicrm_uf_match</table>
+    <key>id</key>
+    <onDelete>CASCADE</onDelete>
+  </foreignKey>
+
+  <field>
+    <name>role_id</name>
+    <type>int unsigned</type>
+    <comment>FK to Role</comment>
+    <html>
+      <label>Role</label>
+      <type>EntityRef</type>
+    </html>
+  </field>
+  <foreignKey>
+    <name>role_id</name>
+    <table>civicrm_role</table>
+    <key>id</key>
+    <onDelete>CASCADE</onDelete>
+  </foreignKey>
+
+</table>