From a763a4def915a3f4347f013429d543489520d97a Mon Sep 17 00:00:00 2001 From: Peter Rotich <peter@osticket.com> Date: Sun, 7 Dec 2014 22:28:46 +0000 Subject: [PATCH] Role-based Access Introduce the concept of role-based access for agents. --- bootstrap.php | 1 + include/class.i18n.php | 4 +- include/class.nav.php | 1 + include/class.role.php | 279 +++++++++++++++++++++++ include/i18n/en_US/role.yaml | 60 +++++ include/staff/role.inc.php | 118 ++++++++++ include/staff/roles.inc.php | 125 ++++++++++ scp/roles.php | 139 +++++++++++ setup/inc/streams/core/install-mysql.sql | 12 + 9 files changed, 738 insertions(+), 1 deletion(-) create mode 100644 include/class.role.php create mode 100644 include/i18n/en_US/role.yaml create mode 100644 include/staff/role.inc.php create mode 100644 include/staff/roles.inc.php create mode 100644 scp/roles.php diff --git a/bootstrap.php b/bootstrap.php index c5a03fe2a..bb9eede35 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -82,6 +82,7 @@ class Bootstrap { define('DEPT_TABLE',$prefix.'department'); define('GROUP_TABLE',$prefix.'groups'); define('GROUP_DEPT_TABLE', $prefix.'group_dept_access'); + define('ROLE_TABLE', $prefix.'role'); define('FAQ_TABLE',$prefix.'faq'); define('FAQ_TOPIC_TABLE',$prefix.'faq_topic'); diff --git a/include/class.i18n.php b/include/class.i18n.php index 915628893..4b764717a 100644 --- a/include/class.i18n.php +++ b/include/class.i18n.php @@ -61,7 +61,9 @@ class Internationalization { // Organization 'organization.yaml' => 'Organization::__create', // Ticket - 'ticket_status.yaml' => 'TicketStatus::__create', + 'ticket_status.yaml' => 'TicketStatus::__create', + // Role + 'role.yaml' => 'Role::__create', // Note that group requires department 'group.yaml' => 'Group::__create', 'file.yaml' => 'AttachmentFile::create', diff --git a/include/class.nav.php b/include/class.nav.php index bc4956f08..30300468f 100644 --- a/include/class.nav.php +++ b/include/class.nav.php @@ -260,6 +260,7 @@ class AdminNav extends StaffNav{ $subnav[]=array('desc'=>__('Agents'),'href'=>'staff.php','iconclass'=>'users'); $subnav[]=array('desc'=>__('Teams'),'href'=>'teams.php','iconclass'=>'teams'); $subnav[]=array('desc'=>__('Groups'),'href'=>'groups.php','iconclass'=>'groups'); + $subnav[]=array('desc'=>__('Roles'),'href'=>'roles.php','iconclass'=>'lists'); $subnav[]=array('desc'=>__('Departments'),'href'=>'departments.php','iconclass'=>'departments'); break; case 'apps': diff --git a/include/class.role.php b/include/class.role.php new file mode 100644 index 000000000..ff3a238e8 --- /dev/null +++ b/include/class.role.php @@ -0,0 +1,279 @@ +<?php +/********************************************************************* + class.role.php + + Role-based access + + Peter Rotich <peter@osticket.com> + Copyright (c) 2014 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ + +class RoleModel extends VerySimpleModel { + static $meta = array( + 'table' => ROLE_TABLE, + 'pk' => array('id'), + 'joins' => array( + 'groups' => array( + 'null' => true, + 'list' => true, + 'reverse' => 'Group.role', + ), + ), + ); + + // Flags + const FLAG_ENABLED = 0x0001; + + protected function hasFlag($flag) { + return ($this->get('flags') & $flag) !== 0; + } + + protected function clearFlag($flag) { + return $this->set('flags', $this->get('flags') & ~$flag); + } + + protected function setFlag($flag) { + return $this->set('flags', $this->get('flags') | $flag); + } + + function getId() { + return $this->id; + } + + function getName() { + return $this->name; + } + + function getCreateDate() { + return $this->created; + } + + function getUpdateDate() { + return $this->updated; + } + + function getInfo() { + return $this->ht; + } + + function isEnabled() { + return $this->hasFlag(self::FLAG_ENABLED); + } + + function isDeleteable() { + return ($this->groups->count() == 0); + } + +} + +class Role extends RoleModel { + var $form; + var $entry; + + var $_perm; + + function getPermission() { + if (!$this->_perm) + $this->_perm = new RolePermission('role.'.$this->getId()); + + return $this->_perm; + } + + function getPermissionInfo() { + return $this->getPermission()->getInfo(); + } + + function to_json() { + + $info = array( + 'id' => $this->getId(), + 'name' => $this->getName() + ); + + return JsonDataEncoder::encode($info); + } + + function __toString() { + return (string) $this->getName(); + } + + private function updatePerms($vars, &$errors=array()) { + + $config = array(); + foreach (RolePermission::allPermissions() as $g => $perms) { + foreach($perms as $k => $v) + $config[$k] = in_array($k, $vars) ? 1 : 0; + } + + $this->getPermission()->updateAll($config); + $this->getPermission()->load(); + } + + function update($vars, &$errors) { + + if (!$vars['name']) + $errors['name'] = __('Name required'); + elseif (($r=Role::lookup(array('name'=>$vars['name']))) + && $r->getId() != $vars['id']) + $errors['name'] = __('Name already in-use'); + elseif (!$vars['perms'] || !count($vars['perms'])) + $errors['err'] = __('Must check at least one permission for the role'); + + if ($errors) + return false; + + $this->name = $vars['name']; + $this->notes = $vars['notes']; + if (!$this->save(true)) + return false; + + $this->updatePerms($vars['perms'], $errors); + + return true; + } + + function save($refetch=false) { + if (count($this->dirty)) + $this->set('updated', new SqlFunction('NOW')); + if (isset($this->dirty['notes'])) + $this->notes = Format::sanitize($this->notes); + + return parent::save($refetch | $this->dirty); + } + + function delete() { + + if (!$this->isDeleteable()) + return false; + + if (!parent::delete()) + return false; + + // Remove dept access entries + GroupDeptAccess::objects() + ->filter(array('role_id'=>$this->getId())) + ->update(array('role_id' => 0)); + + // Delete permission settings + $this->getPermission()->destroy(); + + return true; + } + + static function create($vars=false) { + $role = parent::create($vars); + $role->created = SqlFunction::NOW(); + return $role; + } + + static function __create($vars, &$errors) { + $role = self::create($vars); + $role->save(); + if ($vars['permissions']) + $role->updatePerms($vars['permissions']); + + return $role; + } + + static function getRoles($criteria=null) { + static $roles = null; + + if (!isset($roles) || $criteria) { + + $filters = array(); + if (isset($criteria['enabled'])) { + if ($criteria['enabled']) + $filters += array( + 'flags__hasbit' => self::FLAG_ENABLED); + else + $filters [] = Q::not(array( + 'flags__hasbit' => self::FLAG_ENABLED)); + } + $query = self::objects() + ->order_by('name') + ->values_flat('id', 'name'); + + if ($filters) + $query->filter($filters); + + $names = array(); + foreach ($query as $row) + $names[$row[0]] = $row[1]; + + // TODO: Localize + + if ($criteria) return $names; + + $roles = $names; + } + + return $roles; + } + + static function getActiveRoles() { + static $roles = null; + + if (!isset($roles)) + $roles = self::getRoles(array('enabled' => true)); + + return $roles; + } +} + + +class RolePermission extends Config { + + static $_permissions = array( + /* @trans */ 'Tickets' => array( + 'ticket.create' => array( + /* @trans */ 'Create', + /* @trans */ 'Ability to open tickets on behalf of users'), + 'ticket.edit' => array( + /* @trans */ 'Edit', + /* @trans */ 'Ability to edit tickets'), + 'ticket.assign' => array( + /* @trans */ 'Assign', + /* @trans */ 'Ability to assign tickets to agents or teams'), + 'ticket.transfer' => array( + /* @trans */ 'Transfer', + /* @trans */ 'Ability to transfer tickets between departments'), + 'ticket.reply' => array( + /* @trans */ 'Post Reply', + /* @trans */ 'Ability to post a ticket reply'), + 'ticket.close' => array( + /* @trans */ 'Close', + /* @trans */ 'Ability to close tickets'), + 'ticket.delete' => array( + /* @trans */ 'Delete', + /* @trans */ 'Ability to delete tickets'), + ), + /* @trans */ 'Knowledgebase' => array( + 'kb.premade' => array( + /* @trans */ 'Premade', + /* @trans */ 'Ability to add/update/disable/delete canned responses'), + 'kb.faq' => array( + /* @trans */ 'FAQ', + /* @trans */ 'Ability to add/update/disable/delete knowledgebase categories and FAQs'), + ), + /* @trans */ 'Misc.' => array( + 'stats.agents' => array( + /* @trans */ 'Stats', + /* @trans */ 'Ability to view stats of other agents in allowed departments'), + 'emails.banlist' => array( + /* @trans */ 'Banlist', + /* @trans */ 'Ability to add/remove emails from banlist via ticket interface'), + ), + ); + + static function allPermissions() { + return static::$_permissions; + } + +} +?> diff --git a/include/i18n/en_US/role.yaml b/include/i18n/en_US/role.yaml new file mode 100644 index 000000000..f3def31e2 --- /dev/null +++ b/include/i18n/en_US/role.yaml @@ -0,0 +1,60 @@ +# +# Default roles defined for the system +# +# Fields: +# id - Primary id for the role +# flags - (bit mask) role flags +# name - (string) descriptive name for the role +# notes - (string) internal notes +# permissions: (list<keys>) +# +# NOTE: ------------------------------------ +# --- +- id: 1 + flags: 1 + name: All Access + notes: | + Role with unlimited access + + permissions: [ + ticket.create, + ticket.edit, + ticket.assign, + ticket.transfer, + ticket.reply, + ticket.close, + ticket.delete, + kb.premade, + kb.faq, + stats.agents, + emails.banlist] + +- id: 2 + flags: 1 + name: Expanded Access + notes: | + Role with expanded access + + permissions: [ + ticket.create, + ticket.edit, + ticket.assign, + ticket.transfer, + ticket.reply, + ticket.close, + kb.premade, + kb.faq, + stats.agents, + emails.banlist] + +- id: 3 + flags: 1 + name: Limited Access + notes: | + Role with limited access + + permissions: [ + ticket.create, + ticket.assign, + ticket.transfer, + ticket.reply] diff --git a/include/staff/role.inc.php b/include/staff/role.inc.php new file mode 100644 index 000000000..49e1fff6a --- /dev/null +++ b/include/staff/role.inc.php @@ -0,0 +1,118 @@ +<?php + +$info=array(); +if ($role) { + $title = __('Update Role'); + $action = 'update'; + $submit_text = __('Save Changes'); + $info = $role->getInfo(); + $newcount=2; +} else { + $title = __('Add New Role'); + $action = 'add'; + $submit_text = __('Add Role'); + $newcount=4; +} + +$info = Format::htmlchars(($errors && $_POST) ? array_merge($info, $_POST) : $info); + +?> +<form action="" method="post" id="save"> + <?php csrf_token(); ?> + <input type="hidden" name="do" value="<?php echo $action; ?>"> + <input type="hidden" name="a" value="<?php echo Format::htmlchars($_REQUEST['a']); ?>"> + <input type="hidden" name="id" value="<?php echo $info['id']; ?>"> +<h2> <?php echo $role ?: __('New Role'); ?></h2> +<br> +<ul class="tabs"> + <li class="active"><a href="#definition"> + <i class="icon-file"></i> <?php echo __('Definition'); ?></a></li> + <li><a href="#permissions"> + <i class="icon-lock"></i> <?php echo __('Permissions'); ?></a></li> +</ul> +<div id="definition" class="tab_content"> + <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2"> + <thead> + <tr> + <th colspan="2"> + <h4><?php echo $title; ?></h4> + <em><?php echo __( + 'Roles are used to define agents\' permissions' + ); ?> <i class="help-tip icon-question-sign" + href="#roles"></i></em> + </th> + </tr> + </thead> + <tbody> + <tr> + <td width="180" class="required"><?php echo __('Name'); ?>:</td> + <td> + <input size="50" type="text" name="name" value="<?php echo + $info['name']; ?>"/> + <span class="error">* <?php echo $errors['name']; ?></span> + </td> + </tr> + </tbody> + <tbody> + <tr> + <th colspan="7"> + <em><strong><?php echo __('Internal Notes'); ?></strong> </em> + </th> + </tr> + <tr> + <td colspan="7"><textarea name="notes" class="richtext no-bar" + rows="6" cols="80"><?php + echo $info['notes']; ?></textarea> + </td> + </tr> + </tbody> + </table> +</div> +<div id="permissions" class="tab_content" style="display:none"> + <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2"> + <thead> + <tr> + <th> + <em><?php echo __('Check all permissions applicable to this role.') ?></em> + </th> + </tr> + </thead> + <tbody> + <?php + + $setting = $role ? $role->getPermissionInfo() : array(); + foreach (RolePermission::allPermissions() as $g => $perms) { ?> + <tr><th><?php + echo Format::htmlchars(__($g)); ?></th></tr> + <?php + foreach($perms as $k => $v) { ?> + <tr> + <td> + <label> + <?php + echo sprintf('<input type="checkbox" name="perms[]" value="%s" %s />', + $k, + (isset($setting[$k]) && $setting[$k]) ? 'checked="checked"' : ''); + ?> + + <?php + echo sprintf('%s - <em>%s</em>', + Format::htmlchars(__($v[0])), + Format::htmlchars(__($v[1]))); + ?> + </label> + </td> + </tr> + <?php + } + } ?> + </tbody> + </table> +</div> +<p class="centered"> + <input type="submit" name="submit" value="<?php echo $submit_text; ?>"> + <input type="reset" name="reset" value="<?php echo __('Reset'); ?>"> + <input type="button" name="cancel" value="<?php echo __('Cancel'); ?>" + onclick='window.location.href="?"'> +</p> +</form> diff --git a/include/staff/roles.inc.php b/include/staff/roles.inc.php new file mode 100644 index 000000000..17723f19d --- /dev/null +++ b/include/staff/roles.inc.php @@ -0,0 +1,125 @@ +<div class="pull-left" style="width:700;padding-top:5px;"> + <h2><?php echo __('Roles'); ?></h2> +</div> +<div class="pull-right flush-right" style="padding-top:5px;padding-right:5px;"> + <b><a href="roles.php?a=add" class="Icon list-add"><?php + echo __('Add New Role'); ?></a></b></div> +<div class="clear"></div> + +<?php +$page = ($_GET['p'] && is_numeric($_GET['p'])) ? $_GET['p'] : 1; +$count = Role::objects()->count(); +$pageNav = new Pagenate($count, $page, PAGE_LIMIT); +$pageNav->setURL('roles.php'); +$showing=$pageNav->showing().' '._N('role', 'roles', $count); + +?> +<form action="roles.php" method="POST" name="roles"> +<?php csrf_token(); ?> +<input type="hidden" name="do" value="mass_process" > +<input type="hidden" id="action" name="a" value="" > +<table class="list" border="0" cellspacing="1" cellpadding="0" width="940"> + <caption><?php echo $showing; ?></caption> + <thead> + <tr> + <th width="7"> </th> + <th><?php echo __('Name'); ?></th> + <th width="100"><?php echo __('Status'); ?></th> + <th width="200"><?php echo __('Created On') ?></th> + <th width="250"><?php echo __('Last Updated'); ?></th> + </tr> + </thead> + <tbody> + <?php foreach (Role::objects()->order_by('name') + ->limit($pageNav->getLimit()) + ->offset($pageNav->getStart()) as $role) { + $id = $role->getId(); + $sel = false; + if ($ids && in_array($id, $ids)) + $sel = true; ?> + <tr> + <td> + <?php + if ($role->isDeleteable()) { ?> + <input width="7" type="checkbox" class="ckb" name="ids[]" + value="<?php echo $id; ?>" + <?php echo $sel?'checked="checked"':''; ?>> + <?php + } else { + echo ' '; + } + ?> + </td> + <td><a href="?id=<?php echo $id; ?>"><?php echo + $role->getName(); ?></a></td> + <td> <?php echo $role->isEnabled() ? __('Active') : + '<b>'.__('Disabled').'</b>'; ?></td> + <td><?php echo Format::date($role->getCreateDate()); ?></td> + <td><?php echo Format::datetime($role->getUpdateDate()); ?></td> + </tr> + <?php } + ?> + </tbody> + <tfoot> + <tr> + <td colspan="4"> + <?php if($count){ ?> + <?php echo __('Select'); ?>: + <a id="selectAll" href="#ckb"><?php echo __('All'); ?></a> + <a id="selectNone" href="#ckb"><?php echo __('None'); ?></a> + <a id="selectToggle" href="#ckb"><?php echo __('Toggle'); ?></a> + <?php } else { + echo sprintf(__('No roles defined yet — %s add one %s!'), + '<a href="roles.php?a=add">','</a>'); + } ?> + </td> + </tr> + </tfoot> +</table> +<?php +if ($count) //Show options.. + echo '<div> '.__('Page').':'.$pageNav->getPageLinks().' </div>'; +?> + +<p class="centered" id="actions"> + <input class="button" type="submit" name="enable" value="<?php echo + __('Enable'); ?>"> + + <input class="button" type="submit" name="disable" value="<?php echo + __('Disable'); ?>"> + + <input class="button" type="submit" name="delete" value="<?php echo + __('Delete'); ?>"> +</p> +</form> + +<div style="display:none;" class="dialog" id="confirm-action"> + <h3><?php echo __('Please Confirm'); ?></h3> + <a class="close" href=""><i class="icon-remove-circle"></i></a> + <hr/> + <p class="confirm-action" style="display:none;" id="enable-confirm"> + <?php echo sprintf(__('Are you sure want to <b>enable</b> %s?'), + _N('selected role', 'selected roles', 2));?> + </p> + <p class="confirm-action" style="display:none;" id="disable-confirm"> + <?php echo sprintf(__('Are you sure want to <b>disable</b> %s?'), + _N('selected role', 'selected roles', 2));?> + </p> + <p class="confirm-action" style="display:none;" id="delete-confirm"> + <font color="red"><strong><?php echo sprintf( + __('Are you sure you want to DELETE %s?'), + _N('selected role', 'selected roles', 2)); ?></strong></font> + <br><br><?php echo __('Deleted roles CANNOT be recovered.'); ?> + </p> + <div><?php echo __('Please confirm to continue.'); ?></div> + <hr style="margin-top:1em"/> + <p class="full-width"> + <span class="buttons pull-left"> + <input type="button" value="<?php echo __('No, Cancel'); ?>" class="close"> + </span> + <span class="buttons pull-right"> + <input type="button" value="<?php echo __('Yes, Do it!'); ?>" class="confirm"> + </span> + </p> + <div class="clear"></div> +</div> diff --git a/scp/roles.php b/scp/roles.php new file mode 100644 index 000000000..43c097f5c --- /dev/null +++ b/scp/roles.php @@ -0,0 +1,139 @@ +<?php +/********************************************************************* + roles.php + + Agent's roles + + Peter Rotich <peter@osticket.com> + Copyright (c) 2014 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ + +require('admin.inc.php'); + +$errors = array(); +$role=null; +if ($_REQUEST['id'] && !($role = Role::lookup($_REQUEST['id']))) + $errors['err'] = sprintf(__('%s: Unknown or invalid ID.'), + __('Role')); + +if ($_POST) { + switch (strtolower($_POST['do'])) { + case 'update': + if (!$role) { + $errors['err'] = sprintf(__('%s: Unknown or invalid ID.'), + __('Role')); + } elseif ($role->update($_POST, $errors)) { + $msg = __('Role updated successfully'); + } elseif ($errors) { + $errors['err'] = $errors['err'] ?: + sprintf(__('Unable to update %s. Correct error(s) below and try again!'), + __('this role')); + } else { + $errors['err'] = sprintf(__('Unable to update %s.'), __('this role')) + .' '.__('Internal error occurred'); + } + break; + case 'add': + $_role = Role::create(); + if ($_role->update($_POST, $errors)) { + $msg = sprintf(__('Successfully added %s'), + __('role')); + } elseif ($errors) { + $errors['err'] = $errors['err'] ?: + sprintf(__('Unable to add %s. Correct error(s) below and try again.'), + __('role')); + } else { + $errors['err'] = sprintf(__('Unable to add %s.'), __('role')) + .' '.__('Internal error occurred'); + } + break; + case 'mass_process': + if (!$_POST['ids'] || !is_array($_POST['ids']) || !count($_POST['ids'])) { + $errors['err'] = sprintf(__('You must select at least %s'), + __('one role')); + } else { + $count = count($_POST['ids']); + switch(strtolower($_POST['a'])) { + case 'enable': + $num = Role::objects()->filter(array( + 'id__in' => $_POST['ids'] + ))->update(array( + 'flags'=> SqlExpression::bitor( + new SqlField('flags'), + Role::FLAG_ENABLED) + )); + if ($num) { + if($num==$count) + $msg = sprintf(__('Successfully enabled %s'), + _N('selected role', 'selected roles', $count)); + else + $warn = sprintf(__('%1$d of %2$d %3$s enabled'), $num, $count, + _N('selected role', 'selected roles', $count)); + } else { + $errors['err'] = sprintf(__('Unable to enable %s'), + _N('selected role', 'selected roles', $count)); + } + break; + case 'disable': + $num = Role::objects()->filter(array( + 'id__in' => $_POST['ids'] + ))->update(array( + 'flags'=> SqlExpression::bitand( + new SqlField('flags'), + (~Role::FLAG_ENABLED)) + )); + + if ($num) { + if($num==$count) + $msg = sprintf(__('Successfully disabled %s'), + _N('selected role', 'selected roles', $count)); + else + $warn = sprintf(__('%1$d of %2$d %3$s disabled'), $num, $count, + _N('selected role', 'selected roles', $count)); + } else { + $errors['err'] = sprintf(__('Unable to disable %s'), + _N('selected role', 'selected roles', $count)); + } + break; + case 'delete': + $i=0; + foreach ($_POST['ids'] as $k=>$v) { + if (($r=Role::lookup($v)) && $r->isDeleteable() && $r->delete()) + $i++; + } + if ($i && $i==$count) + $msg = sprintf(__('Successfully deleted %s'), + _N('selected role', 'selected roles', $count)); + elseif ($i > 0) + $warn = sprintf(__('%1$d of %2$d %3$s deleted'), $i, $count, + _N('selected role', 'selected roles', $count)); + elseif (!$errors['err']) + $errors['err'] = sprintf(__('Unable to delete %s — they may be in use.'), + _N('selected role', 'selected roles', $count)); + break; + default: + $errors['err'] = __('Unknown action'); + } + } + break; + } +} + +$page='roles.inc.php'; +if($role || ($_REQUEST['a'] && !strcasecmp($_REQUEST['a'], 'add'))) { + $page='role.inc.php'; + $ost->addExtraHeader('<meta name="tip-namespace" content="agents.role" />', + "$('#content').data('tipNamespace', 'agents.role');"); +} + +$nav->setTabActive('staff'); +require(STAFFINC_DIR.'header.inc.php'); +require(STAFFINC_DIR.$page); +include(STAFFINC_DIR.'footer.inc.php'); +?> diff --git a/setup/inc/streams/core/install-mysql.sql b/setup/inc/streams/core/install-mysql.sql index a23a71b65..900a1f47f 100644 --- a/setup/inc/streams/core/install-mysql.sql +++ b/setup/inc/streams/core/install-mysql.sql @@ -411,6 +411,18 @@ CREATE TABLE `%TABLE_PREFIX%groups` ( KEY `group_active` (`group_enabled`) ) DEFAULT CHARSET=utf8; +DROP TABLE IF EXISTS `%TABLE_PREFIX%role`; +CREATE TABLE `%TABLE_PREFIX%role` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `flags` int(10) unsigned NOT NULL DEFAULT '1', + `name` varchar(64) DEFAULT NULL, + `notes` text, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `name` (`name`) +) DEFAULT CHARSET=utf8; + DROP TABLE IF EXISTS `%TABLE_PREFIX%group_dept_access`; CREATE TABLE `%TABLE_PREFIX%group_dept_access` ( `group_id` int(10) unsigned NOT NULL default '0', -- GitLab