* Admin: allow to mark users as hidden, to show them only to admins e.g. to hide functional users from regular users

This commit is contained in:
ralf 2024-11-05 14:01:18 +01:00
parent d31915df05
commit e72c2e492a
13 changed files with 116 additions and 11 deletions

View File

@ -69,6 +69,7 @@ class admin_account
$acl = new Acl($content['account_id']); $acl = new Acl($content['account_id']);
$acl->read_repository(); $acl->read_repository();
$account['anonymous'] = $acl->check('anonymous', 1, 'phpgwapi'); $account['anonymous'] = $acl->check('anonymous', 1, 'phpgwapi');
$account['hidden'] = $acl->check('hidden', 1, 'phpgwapi');
$account['changepassword'] = !$acl->check('nopasswordchange', 1, 'preferences'); $account['changepassword'] = !$acl->check('nopasswordchange', 1, 'preferences');
$auth = new Api\Auth(); $auth = new Api\Auth();
if (($account['account_lastpwd_change'] = $auth->getLastPwdChange($account['account_lid'])) === false) if (($account['account_lastpwd_change'] = $auth->getLastPwdChange($account['account_lid'])) === false)
@ -116,7 +117,7 @@ class admin_account
// save old values to only trigger save, if one of the following values change (contact data get saved anyway) // save old values to only trigger save, if one of the following values change (contact data get saved anyway)
$preserve = empty($content['id']) ? array() : $preserve = empty($content['id']) ? array() :
array('old_account' => array_intersect_key($account, array_flip(array( array('old_account' => array_intersect_key($account, array_flip(array(
'account_lid', 'account_status', 'account_groups', 'anonymous', 'changepassword', 'account_lid', 'account_status', 'account_groups', 'anonymous', 'hidden', 'changepassword',
'mustchangepassword', 'account_primary_group', 'homedirectory', 'loginshell', 'mustchangepassword', 'account_primary_group', 'homedirectory', 'loginshell',
'account_expires', 'account_firstname', 'account_lastname', 'account_email'))), 'account_expires', 'account_firstname', 'account_lastname', 'account_email'))),
'deny_edit' => $deny_edit); 'deny_edit' => $deny_edit);
@ -185,7 +186,7 @@ class admin_account
'account_groups', 'account_groups',
// copy following fields to account // copy following fields to account
'account_lid', 'account_lid',
'changepassword', 'anonymous', 'mustchangepassword', 'changepassword', 'anonymous', 'hidden', 'mustchangepassword',
'account_passwd', 'account_passwd_2', 'account_passwd', 'account_passwd_2',
'account_primary_group', 'account_primary_group',
'account_expires', 'account_status', 'account_expires', 'account_status',
@ -215,6 +216,7 @@ class admin_account
case 'changepassword': // boolean values: admin_cmd_edit_user understands '' as NOT set case 'changepassword': // boolean values: admin_cmd_edit_user understands '' as NOT set
case 'anonymous': case 'anonymous':
case 'hidden':
case 'mustchangepassword': case 'mustchangepassword':
$account[$a_name] = (boolean)$content[$c_name]; $account[$a_name] = (boolean)$content[$c_name];
break; break;

View File

@ -107,6 +107,7 @@ class admin_cmd_edit_user extends admin_cmd_change_pw
// automatic set anonymous flag for username "anonymous", to not allow to create anonymous user without it // automatic set anonymous flag for username "anonymous", to not allow to create anonymous user without it
$data['anonymous'] = ($data['account_lid'] ?: admin_cmd::$accounts->id2name($this->account)) === 'anonymous' ? $data['anonymous'] = ($data['account_lid'] ?: admin_cmd::$accounts->id2name($this->account)) === 'anonymous' ?
true : admin_cmd::parse_boolean($data['anonymous'],$this->account ? null : false); true : admin_cmd::parse_boolean($data['anonymous'],$this->account ? null : false);
$data['hidden'] = admin_cmd::parse_boolean($data['hidden'],null);
if ($data['mustchangepassword'] && $data['changepassword']) if ($data['mustchangepassword'] && $data['changepassword'])
{ {
@ -187,6 +188,18 @@ class admin_cmd_edit_user extends admin_cmd_change_pw
admin_cmd::$acl->delete_repository('phpgwapi','anonymous',$data['account_id']); admin_cmd::$acl->delete_repository('phpgwapi','anonymous',$data['account_id']);
} }
} }
if (isset($data['hidden']))
{
admin_cmd::_instanciate_acl();
if ($data['hidden'])
{
admin_cmd::$acl->add_repository('phpgwapi','hidden',$data['account_id'],1);
}
else
{
admin_cmd::$acl->delete_repository('phpgwapi','hidden',$data['account_id']);
}
}
if (!is_null($data['changepassword'])) if (!is_null($data['changepassword']))
{ {
if (!$data['changepassword']) if (!$data['changepassword'])

View File

@ -122,6 +122,7 @@ anonymous user admin de Anonymer Benutzer
anonymous user (not shown in list sessions) admin de Anonymer Benutzer (wird bei 'Sitzungen anzeigen' nicht angezeigt). anonymous user (not shown in list sessions) admin de Anonymer Benutzer (wird bei 'Sitzungen anzeigen' nicht angezeigt).
anonymous user does not exist! admin de Anonymer Benutzer existiert NICHT! anonymous user does not exist! admin de Anonymer Benutzer existiert NICHT!
anonymous user has no run-rights for the application! admin de Anonymer Benutzer hat KEINE Ausführungsrechte für die Anwendung! anonymous user has no run-rights for the application! admin de Anonymer Benutzer hat KEINE Ausführungsrechte für die Anwendung!
anonymous user. not shown in list sessions. admin de Anonymer Benutzer. Wird nicht in der Liste der Sitzungen angezeigt.
any application admin de Jede Anwendung any application admin de Jede Anwendung
any group admin de Jede Gruppe any group admin de Jede Gruppe
any user admin de Jeder Benutzer any user admin de Jeder Benutzer
@ -958,6 +959,7 @@ user csv import admin de CSV-Import von Benutzern
user data common de Benutzerdaten user data common de Benutzerdaten
user for smtp-authentication (leave it empty if no auth required) admin de Benutzer für SMTP-Authentifizierung (leer lassen falls keine Authentifizierung nötig). user for smtp-authentication (leave it empty if no auth required) admin de Benutzer für SMTP-Authentifizierung (leer lassen falls keine Authentifizierung nötig).
user groups admin de Benutzergruppen user groups admin de Benutzergruppen
user hidden vom non-admins. admin de Benutzer verborgen von nicht Administratoren.
user-agent admin de Browser user-agent admin de Browser
userdata admin de Benutzerkonto userdata admin de Benutzerkonto
userid@domain eg. u1234@domain admin de UserId@domain z.B. u1234@domain userid@domain eg. u1234@domain admin de UserId@domain z.B. u1234@domain

View File

@ -122,6 +122,7 @@ anonymous user admin en Anonymous user
anonymous user (not shown in list sessions) admin en Anonymous user. Not shown in sessions list. anonymous user (not shown in list sessions) admin en Anonymous user. Not shown in sessions list.
anonymous user does not exist! admin en Anonymous user does NOT exist! anonymous user does not exist! admin en Anonymous user does NOT exist!
anonymous user has no run-rights for the application! admin en Anonymous user has NO run-rights for the application! anonymous user has no run-rights for the application! admin en Anonymous user has NO run-rights for the application!
anonymous user. not shown in list sessions. admin en Anonymous user. Not shown in list sessions.
any application admin en Any application any application admin en Any application
any group admin en Any group any group admin en Any group
any user admin en Any user any user admin en Any user
@ -958,6 +959,7 @@ user csv import admin en User CSV import
user data common en User data user data common en User data
user for smtp-authentication (leave it empty if no auth required) admin en User for SMTP authentication. Leave it empty if no authentication is required. user for smtp-authentication (leave it empty if no auth required) admin en User for SMTP authentication. Leave it empty if no authentication is required.
user groups admin en User groups user groups admin en User groups
user hidden vom non-admins. admin en User hidden vom non-admins.
user-agent admin en User-Agent user-agent admin en User-Agent
userdata admin en User data userdata admin en User data
userid@domain eg. u1234@domain admin en UserId@domain eg. u1234@domain userid@domain eg. u1234@domain admin en UserId@domain eg. u1234@domain

View File

@ -39,7 +39,10 @@
</et2-vbox> </et2-vbox>
<et2-description></et2-description> <et2-description></et2-description>
<et2-description></et2-description> <et2-description></et2-description>
<et2-checkbox id="anonymous" label="Anonymous user. Not shown in list sessions."></et2-checkbox> <et2-vbox>
<et2-checkbox id="anonymous" label="Anonymous user. Not shown in list sessions."></et2-checkbox>
<et2-checkbox id="hidden" label="User hidden vom non-admins."></et2-checkbox>
</et2-vbox>
<et2-description></et2-description> <et2-description></et2-description>
</row> </row>
<row disabled="!@ldap_extra_attributes"> <row disabled="!@ldap_extra_attributes">

View File

@ -199,6 +199,32 @@ class Accounts
return $key; return $key;
} }
/**
* Implement hidden account-filter via explicit account_id's and "hidden" ACL location
*
* @param bool|null $hidden false: do NOT return hidden users, true: return only hidden user, null: return all users
* @param array|null $account_ids account_id filter, if set
* @return array|int[] array of account_id's optionally with extra "!" for $hidden === false
*/
public static function hidden2account_id(bool $hidden=null, array $account_ids=null)
{
$hidden_account_ids = array_keys($GLOBALS['egw']->acl->get_all_rights('hidden', 'phpgwapi') ?: []);
if (empty($account_ids))
{
$account_ids = $hidden_account_ids;
if ($hidden === false) $account_ids[] = '!';
}
elseif ($hidden === true)
{
$account_ids = array_intersect($account_ids, $hidden_account_ids);
}
else
{
throw new \InvalidArgumentException(__METHOD__."(): Can NOT have hidden=false AND an account_id filter!");
}
return $account_ids;
}
/** /**
* Searches / lists accounts: users and/or groups * Searches / lists accounts: users and/or groups
* *
@ -225,6 +251,7 @@ class Accounts
* 'lid','firstname','lastname','email' - query only the given field for containing $param[query] * 'lid','firstname','lastname','email' - query only the given field for containing $param[query]
* @param $param['app'] string with an app-name, to limit result on accounts with run-right for that app * @param $param['app'] string with an app-name, to limit result on accounts with run-right for that app
* @param $param['active']=true boolean - true: return only acctive accounts, false: return expired or deactivated too * @param $param['active']=true boolean - true: return only acctive accounts, false: return expired or deactivated too
* @param $param['hidden']=false boolean false: do NOT return hidden users, true: return only hidden user, null: return all users
* @param $param['account_id'] int[] return only given account_id's * @param $param['account_id'] int[] return only given account_id's
* @return array with account_id => data pairs, data is an array with account_id, account_lid, account_firstname, * @return array with account_id => data pairs, data is an array with account_id, account_lid, account_firstname,
* account_lastname, person_id (id of the linked addressbook entry), account_status, account_expires, account_primary_group * account_lastname, person_id (id of the linked addressbook entry), account_status, account_expires, account_primary_group
@ -234,6 +261,8 @@ class Accounts
//error_log(__METHOD__.'('.array2string($param).') '.function_backtrace()); //error_log(__METHOD__.'('.array2string($param).') '.function_backtrace());
if (!isset($param['active'])) $param['active'] = true; // default is true = only return active accounts if (!isset($param['active'])) $param['active'] = true; // default is true = only return active accounts
if (!empty($param['offset']) && !isset($param['start'])) $param['start'] = 0; if (!empty($param['offset']) && !isset($param['start'])) $param['start'] = 0;
// show hidden users by default only to admin
if (!array_key_exists('hidden', $param)) $param['hidden'] = !empty($GLOBALS['egw_info']['user']['apps']['admin']) ? null : false;
// Check for lang(Group) in search - if there, we search all groups // Check for lang(Group) in search - if there, we search all groups
$group_index = array_search(strtolower(lang('Group')), array_map('strtolower', $query = explode(' ',$param['query'] ?? ''))); $group_index = array_search(strtolower(lang('Group')), array_map('strtolower', $query = explode(' ',$param['query'] ?? '')));
@ -259,6 +288,13 @@ class Accounts
$account_search = &self::$cache['account_search']; $account_search = &self::$cache['account_search'];
$serial = self::cacheKey($param, $serial_unlimited); $serial = self::cacheKey($param, $serial_unlimited);
// implement $param['hidden'] via $param['account_id']
if (isset($param['hidden']) && !in_array($param['type'],['groups', 'owngroups']))
{
$param['account_id'] = self::hidden2account_id($param['hidden'], $param['account_id']);
}
unset($param['hidden']);
// cache list of all groups on instance level (not session) // cache list of all groups on instance level (not session)
if ($serial_unlimited === self::cacheKey(['type'=>'groups','active'=>true])) if ($serial_unlimited === self::cacheKey(['type'=>'groups','active'=>true]))
{ {
@ -312,7 +348,7 @@ class Accounts
} }
$param['type'] = $param['type'] == 'groupmembers+memberships' ? 'both' : 'accounts'; $param['type'] = $param['type'] == 'groupmembers+memberships' ? 'both' : 'accounts';
} }
// call ourself recursive to get (evtl. cached) full search // call ourselves recursive to get (evtl. cached) full search
$full_search = $this->search($param); $full_search = $this->search($param);
// filter search now on accounts with run-rights for app or a group // filter search now on accounts with run-rights for app or a group

View File

@ -1062,7 +1062,15 @@ class Ads
} }
if (!empty($param['account_id'])) if (!empty($param['account_id']))
{ {
if (($not_account_ids = array_search('!', $param['account_id'])) !== false)
{
unset($param['account_id'][$not_account_ids]);
}
$account_ids_filter = '(|(objectsid='.implode(')(objectsid=', array_map([$this, 'get_sid'], (array)$param['account_id'])).'))'; $account_ids_filter = '(|(objectsid='.implode(')(objectsid=', array_map([$this, 'get_sid'], (array)$param['account_id'])).'))';
if ($not_account_ids !== false)
{
$account_ids_filter = '(!'.$account_ids_filter.')';
}
$filter = $filter ? "(&$filter$account_ids_filter)" : $account_ids_filter; $filter = $filter ? "(&$filter$account_ids_filter)" : $account_ids_filter;
} }
if (!empty($param['modified'])) if (!empty($param['modified']))

View File

@ -842,7 +842,17 @@ class Ldap
// only return given account_id's // only return given account_id's
if (!empty($param['account_id'])) if (!empty($param['account_id']))
{ {
// do we need a negated account_id filter
if (($not_account_ids = array_search('!', $param['account_id'])) !== false)
{
$filter .= '(!';
unset($param['account_id'][$not_account_ids]);
}
$filter .= '(|(uidNumber=' . implode(')(uidNumber=', (array)$param['account_id']) . '))'; $filter .= '(|(uidNumber=' . implode(')(uidNumber=', (array)$param['account_id']) . '))';
if ($not_account_ids !== false)
{
$filter .= ')';
}
} }
if (!empty($param['modified'])) if (!empty($param['modified']))
{ {

View File

@ -510,7 +510,11 @@ class Sql
$this->total = 0; $this->total = 0;
return array(); return array();
} }
$filter[] = $this->db->expression($this->table, $this->table.'.', array( if (($not_account_id = array_search('!', $filter['account_id'])) !== false)
{
unset($filter['account_id'][$not_account_id]);
}
$filter[] = ($not_account_id !== false ? ' NOT ' : '').$this->db->expression($this->table, $this->table.'.', array(
'account_id' => $filter['account_id'], 'account_id' => $filter['account_id'],
)); ));
unset($filter['account_id']); unset($filter['account_id']);

View File

@ -185,7 +185,7 @@ class Ads extends Ldap
/** /**
* Return LDAP filter for (multiple) account ids * Return LDAP filter for (multiple) account ids
* *
* @param int|int[]|null $ids * @param int|int[]|null $ids use "!" to negate the whole filter
* @return string * @return string
*/ */
protected function account_ids_filter($ids) protected function account_ids_filter($ids)
@ -197,12 +197,20 @@ class Ads extends Ldap
} }
elseif ($ids) elseif ($ids)
{ {
if (($not_account_ids = array_search('!', $ids)) !== false)
{
unset($ids[$not_account_ids]);
}
if (is_array($ids)) $filter = '(|'; if (is_array($ids)) $filter = '(|';
foreach((array)$ids as $account_id) foreach((array)$ids as $account_id)
{ {
$filter .= '(objectsid='.$this->accounts_ads->get_sid($account_id).')'; $filter .= '(objectsid='.$this->accounts_ads->get_sid($account_id).')';
} }
if (is_array($ids)) $filter .= ')'; if (is_array($ids)) $filter .= ')';
if ($not_account_ids !== false)
{
$filter = '(!'.$filter.')';
}
} }
return $filter; return $filter;
} }

View File

@ -470,10 +470,11 @@ class Ldap
* Return LDAP filter for (multiple) contact ids * Return LDAP filter for (multiple) contact ids
* *
* @param array|string $ids * @param array|string $ids
* @param bool $not=false true: negate the filter
* @throws Api\Exception\AssertionFailed if $contact_id is no valid GUID (for ADS!) * @throws Api\Exception\AssertionFailed if $contact_id is no valid GUID (for ADS!)
* @return string * @return string
*/ */
protected function ids_filter($ids) protected function ids_filter($ids, bool $not=false)
{ {
if (!is_array($ids) || count($ids) == 1) if (!is_array($ids) || count($ids) == 1)
{ {
@ -484,13 +485,13 @@ class Ldap
{ {
$filter[] = $this->id_filter($id); $filter[] = $this->id_filter($id);
} }
return '(|'.implode('', $filter).')'; return ($not ? '(!' : '').'(|'.implode('', $filter).')'.($not ? ')' : '');
} }
/** /**
* Return LDAP filter for (multiple) account ids * Return LDAP filter for (multiple) account ids
* *
* @param int|int[]|null $ids * @param int|int[]|null $ids use "!" to negate the whole filter
* @return string * @return string
*/ */
protected function account_ids_filter($ids) protected function account_ids_filter($ids)
@ -502,10 +503,14 @@ class Ldap
} }
elseif ($ids) elseif ($ids)
{ {
if (($not_account_ids = array_search('!', $ids)) !== false)
{
unset($ids[$not_account_ids]);
}
$filter = $this->ids_filter(array_map(static function($account_id) $filter = $this->ids_filter(array_map(static function($account_id)
{ {
return $GLOBALS['egw']->accounts->id2name($account_id, 'person_id'); return $GLOBALS['egw']->accounts->id2name($account_id, 'person_id');
}, (array)$ids)); }, (array)$ids), $not_account_ids !== false);
} }
return $filter; return $filter;
} }

View File

@ -716,6 +716,13 @@ class Sql extends Api\Storage
$this->sanitize_order_by = false; $this->sanitize_order_by = false;
} }
// implement negated account_id filter
if (!empty($filter['account_id']) && ($not_account_ids = array_search('!', $filter['account_id'])) !== false)
{
$filter[] = $this->db->expression($this->table_name, ' NOT ', $this->table_name.'.', ['account_id' => $filter['account_id']]);
unset($filter['account_id']);
}
$rows =& parent::search($criteria,$only_keys,$order_by,$extra_cols,$wildcard,$empty,$op,$start,$filter,$join,$need_full_no_count); $rows =& parent::search($criteria,$only_keys,$order_by,$extra_cols,$wildcard,$empty,$op,$start,$filter,$join,$need_full_no_count);
if ($start === false) $this->total = is_array($rows) ? count($rows) : 0; // so_sql sets total only for $start !== false! if ($start === false) $this->total = is_array($rows) ? count($rows) : 0; // so_sql sets total only for $start !== false!

View File

@ -659,6 +659,11 @@ class Storage
{ {
//error_log(__METHOD__.'('.array2string($criteria,true).','.array2string($only_keys).",'$order_by','$extra_cols','$wildcard','$empty','$op',".array2string($start).','.array2string($filter,true).",'$join')"); //error_log(__METHOD__.'('.array2string($criteria,true).','.array2string($only_keys).",'$order_by','$extra_cols','$wildcard','$empty','$op',".array2string($start).','.array2string($filter,true).",'$join')");
// add hidden user filter for non-admins
if (empty($GLOBALS['egw_info']['user']['apps']['admin']) && empty($filter['owner']))
{
$filter['account_id'] = Api\Accounts::hidden2account_id(false, $filter['account_id']);
}
// Handle 'None' country option // Handle 'None' country option
if(is_array($filter) && isset($filter['adr_one_countrycode']) && $filter['adr_one_countrycode'] === '-custom-') if(is_array($filter) && isset($filter['adr_one_countrycode']) && $filter['adr_one_countrycode'] === '-custom-')
{ {