WIP Mail REST API: regular user UI for application passwords

This commit is contained in:
ralf 2023-07-11 13:39:46 +02:00
parent 96bb3a6884
commit 106ead2c8e
8 changed files with 196 additions and 44 deletions

View File

@ -43,7 +43,7 @@ class Token
} }
/** /**
* Edit a host * Edit or add a token
* *
* @param array $content =null * @param array $content =null
*/ */
@ -61,17 +61,24 @@ class Token
else else
{ {
$content = $this->token->init()+['new_token' => true]; $content = $this->token->init()+['new_token' => true];
if (empty($GLOBALS['egw_info']['user']['apps']['admin'])) }
{ if (static::APP !== 'admin')
$content['account_id'] = $GLOBALS['egw_info']['user']['account_id']; {
} Api\Translation::add_app('admin');
} }
} }
elseif (!empty($content['button'])) elseif (!empty($content['button']))
{ {
try { $button = key($content['button'] ?? []);
$button = key($content['button']); unset($content['button']);
if ($button !== 'cancel' && static::APP !== 'admin' &&
!(new Api\Auth())->authenticate($GLOBALS['egw_info']['user']['account_lid'], $content['password']))
{
Api\Etemplate::set_validation_error('password', lang('Password is invalid'));
unset($content['button']); unset($content['button']);
}
try {
switch($button) switch($button)
{ {
case 'save': case 'save':
@ -81,6 +88,10 @@ class Token
{ {
$content['new_token'] = true; $content['new_token'] = true;
$button = 'apply'; // must not close window to show token $button = 'apply'; // must not close window to show token
if (empty($GLOBALS['egw_info']['user']['apps']['admin']) || static::APP !== 'admin')
{
$content['account_id'] = $GLOBALS['egw_info']['user']['account_id'];
}
} }
$this->token->save($content); $this->token->save($content);
Api\Framework::refresh_opener(empty($content['new_token']) ? lang('Token saved.') : lang('Token created.'), Api\Framework::refresh_opener(empty($content['new_token']) ? lang('Token saved.') : lang('Token created.'),
@ -99,6 +110,10 @@ class Token
self::APP, $content['token_id'], 'update'); self::APP, $content['token_id'], 'update');
Api\Framework::window_close(); // does NOT return Api\Framework::window_close(); // does NOT return
break; break;
case 'cancel':
Api\Framework::window_close(); // does NOT return
break;
} }
} }
catch(\Exception $e) { catch(\Exception $e) {
@ -106,25 +121,49 @@ class Token
} }
} }
$content['token_apps'] = Api\Auth\Token::limits2apps($content['token_limits']); $content['token_apps'] = Api\Auth\Token::limits2apps($content['token_limits']);
$content['admin'] = !empty($GLOBALS['egw_info']['user']['apps']['admin']) && static::APP === 'admin';
if (empty($content['account_id'])) $content['account_id'] = ''; if (empty($content['account_id'])) $content['account_id'] = '';
$readonlys = [ $readonlys = [
'button[delete]' => !$content['token_id'], 'button[delete]' => !$content['token_id'],
'account_id' => empty($GLOBALS['egw_info']['user']['apps']['admin']), 'account_id' => empty($GLOBALS['egw_info']['user']['apps']['admin']) || static::APP !== 'admin',
]; ];
$tmpl = new Api\Etemplate(self::APP.'.token.edit'); $tmpl = new Api\Etemplate(self::APP.'.token.edit');
$tmpl->exec(self::APP.'.'.self::class.'.edit', $content, [], $readonlys, $content, 2); $tmpl->exec(static::APP.'.'.static::class.'.edit', $content, [], $readonlys, $content, 2);
} }
/** /**
* Fetch rows to display * Fetch rows to display
* *
* @param array $query * @param array $query with keys 'start', 'search', 'order', 'sort', 'col_filter'
* @param array& $rows =null * For other keys like 'filter', 'cat_id' you have to reimplement this method in a derived class.
* @param array& $readonlys =null * @param array &$rows returned rows/competitions
* @param array &$readonlys eg. to disable buttons based on acl, not use here, maybe in a derived class
* @param string $join ='' sql to do a join, added as is after the table-name, eg. ", table2 WHERE x=y" or
* "LEFT JOIN table2 ON (x=y)", Note: there's no quoting done on $join!
* @param boolean $need_full_no_count =false If true an unlimited query is run to determine the total number of rows, default false
* @param mixed $only_keys =false, see search
* @param string|array $extra_cols =array()
* @return int total number of rows
*/ */
public function get_rows($query, array &$rows=null, array &$readonlys=null) function get_rows($query,&$rows,&$readonlys,$join='',$need_full_no_count=false,$only_keys=false,$extra_cols=array())
{ {
$total = $this->token->get_rows($query, $rows, $readonlys); // do NOT show all users or other users to non-admin or regular user UI
if (empty($GLOBALS['egw_info']['user']['apps']['admin']) || static::APP !== 'admin')
{
$query['col_filter']['account_id'] = $GLOBALS['egw_info']['user']['account_id'];
}
// sort revoked token behind active ones
if (empty($query['order']) || $query['order'] === 'token_id')
{
$order_by = 'token_revoked IS NOT NULL,token_id '.($query['sort'] ?? 'DESC').',token_revoked '.($query['sort'] ?? 'DESC');
}
else
{
$order_by = $query['order'].' '.$query['sort'];
}
$rows = $this->token->search($query['critera'] ?? '', $only_keys, $order_by, $extra_cols,
'',false, 'AND',$query['num_rows']?array((int)$query['start'],$query['num_rows']):(int)$query['start'],
$query['col_filter'],$join,$need_full_no_count) ?: [];
foreach($rows as &$row) foreach($rows as &$row)
{ {
$row['token_apps'] = Api\Auth\Token::limits2apps($row['token_limits']); $row['token_apps'] = Api\Auth\Token::limits2apps($row['token_limits']);
@ -133,7 +172,7 @@ class Token
$row['class'] = 'revoked'; $row['class'] = 'revoked';
} }
} }
return $total; return $this->token->total;
} }
/** /**
@ -143,28 +182,17 @@ class Token
*/ */
public function index(array $content=null) public function index(array $content=null)
{ {
if (!is_array($content) || empty($content['nm'])) if (!is_array($content) || empty($content['token']))
{ {
$content = [ $content = [
'nm' => [ 'token' => self::get_nm_options(),
'get_rows' => self::APP.'.'.__CLASS__.'.get_rows',
'no_filter' => true, // disable the diverse filters we not (yet) use
'no_filter2' => true,
'no_cat' => true,
'order' => 'token_id',// IO name of the column to sort after (optional for the sortheaders)
'sort' => 'DESC',// IO direction of the sort: 'ASC' or 'DESC'
'row_id' => 'token_id',
'actions' => $this->get_actions(),
'placeholder_actions' => array('add'),
'add_link' => Api\Egw::link('/index.php', 'menuaction='.self::APP.'.'.self::class.'.edit'),
]
]; ];
} }
elseif(!empty($content['nm']['action'])) elseif(!empty($content['token']['action']))
{ {
try { try {
Api\Framework::message($this->action($content['nm']['action'], Api\Framework::message($this->action($content['token']['action'],
$content['nm']['selected'], $content['nm']['select_all'])); $content['token']['selected'], $content['token']['select_all']));
} }
catch (\Exception $ex) { catch (\Exception $ex) {
Api\Framework::message($ex->getMessage(), 'error'); Api\Framework::message($ex->getMessage(), 'error');
@ -173,7 +201,28 @@ class Token
$tmpl = new Api\Etemplate(self::APP.'.tokens'); $tmpl = new Api\Etemplate(self::APP.'.tokens');
$tmpl->exec(self::APP.'.'.self::class.'.index', $content, [ $tmpl->exec(self::APP.'.'.self::class.'.index', $content, [
'account_id' => ['0' => lang('All users')] 'account_id' => ['0' => lang('All users')]
], [], ['nm' => $content['nm']]); ], [], ['token' => $content['token']]);
}
/**
* Options for NM widget
*
* @return array
*/
protected static function get_nm_options()
{
return [
'get_rows' => static::APP.'.'.static::class.'.get_rows',
'no_filter' => true, // disable the diverse filters we not (yet) use
'no_filter2' => true,
'no_cat' => true,
'order' => 'token_id',// IO name of the column to sort after (optional for the sortheaders)
'sort' => 'DESC',// IO direction of the sort: 'ASC' or 'DESC'
'row_id' => 'token_id',
'actions' => self::get_actions(static::APP),
'placeholder_actions' => array('add'),
'add_action' => "egw.open_link('".Api\Egw::link('/index.php', 'menuaction='.static::APP.'.'.static::class.'.edit')."','_blank','600x380')",
];
} }
/** /**
@ -182,9 +231,9 @@ class Token
* @param array $cont values for keys license_(nation|year|cat) * @param array $cont values for keys license_(nation|year|cat)
* @return array * @return array
*/ */
protected function get_actions() public static function get_actions(string $app='admin')
{ {
return [ $actions = [
'edit' => [ 'edit' => [
'caption' => 'Edit', 'caption' => 'Edit',
'default' => true, 'default' => true,
@ -213,6 +262,18 @@ class Token
'group' => $group, 'group' => $group,
], ],
]; ];
if ($app === 'preferences')
{
foreach([
'edit' => 'app.preferences.editToken',
'add' => 'app.preferences.addToken',
] as $action => $exec)
{
$actions[$action]['onExecute'] = 'javaScript:'.$exec;
unset($actions[$action]['url'], $actions[$action]['popup']);
}
}
return $actions;
} }
/** /**

View File

@ -12,7 +12,11 @@
<et2-description value="Token"></et2-description> <et2-description value="Token"></et2-description>
<et2-textbox id="token" readonly="true" onclick="app.admin.copyClipboard(this)" class="token"></et2-textbox> <et2-textbox id="token" readonly="true" onclick="app.admin.copyClipboard(this)" class="token"></et2-textbox>
</row> </row>
<row> <row disabled="@admin">
<et2-description for="password" value="Current password"></et2-description>
<et2-password id="password" required="true"></et2-password>
</row>
<row disabled="!@admin">
<et2-description for="account_id" value="User"></et2-description> <et2-description for="account_id" value="User"></et2-description>
<et2-select-account id="account_id" accountType="accounts" emptyLabel="All users"></et2-select-account> <et2-select-account id="account_id" accountType="accounts" emptyLabel="All users"></et2-select-account>
</row> </row>
@ -56,11 +60,11 @@
<et2-description></et2-description> <et2-description></et2-description>
<et2-checkbox id="new_token" label="Generate new token and display it once after saving" span="all"></et2-checkbox> <et2-checkbox id="new_token" label="Generate new token and display it once after saving" span="all"></et2-checkbox>
</row> </row>
<row> <row class="dialogFooterToolbar">
<et2-hbox span="all"> <et2-hbox span="all">
<et2-button accesskey="s" label="Save" id="button[save]"></et2-button> <et2-button accesskey="s" label="Save" id="button[save]"></et2-button>
<et2-button label="Apply" id="button[apply]"></et2-button> <et2-button label="Apply" id="button[apply]"></et2-button>
<et2-button label="Cancel" id="button[cancel]" onclick="window.close(); return false;"></et2-button> <et2-button label="Cancel" id="button[cancel]" noValidation="true"></et2-button>
<et2-button align="right" label="Revoke" id="button[delete]" <et2-button align="right" label="Revoke" id="button[delete]"
onclick="et2_dialog.confirm(widget,'Revoke this token','Revoke')"></et2-button> onclick="et2_dialog.confirm(widget,'Revoke this token','Revoke')"></et2-button>
</et2-hbox> </et2-hbox>

View File

@ -58,9 +58,9 @@
</grid> </grid>
</template> </template>
<template id="admin.tokens.add" template="" lang="" group="0" version="1.9.001"> <template id="admin.tokens.add" template="" lang="" group="0" version="1.9.001">
<et2-button label="Add" id="add" onclick="window.open('$cont[add_link]','_blank','dependent=yes,width=600,height=380,scrollbars=yes,status=yes'); return false;" noSubmit="true"></et2-button> <et2-button label="Add" id="add" onclick="@add_action" noSubmit="true"></et2-button>
</template> </template>
<template id="admin.tokens" template="" lang="" group="0" version="1.9.001"> <template id="admin.tokens" template="" lang="" group="0" version="1.9.001">
<nextmatch id="nm" template="admin.tokens.rows" header_left="admin.tokens.add"/> <nextmatch id="token" template="admin.tokens.rows" header_left="admin.tokens.add"/>
</template> </template>
</overlay> </overlay>

View File

@ -173,12 +173,12 @@ class preferences_password
{ {
$tabs = array(); $tabs = array();
} }
// register hooks, if openid is available, but new hook not yet registered (should be removed after 19.1) // register hooks, if new "application password" hook not yet registered (should be removed after 24.1)
if (!empty($GLOBALS['egw_info']['apps']['openid']) && !Api\Hooks::implemented('preferences_security')) if (!in_array('preferences', array_keys(Api\Hooks::implemented('preferences_security'))))
{ {
Api\Hooks::read(true); Api\Hooks::read(true);
} }
$hook_data = Api\Hooks::process(array('location' => 'preferences_security')+$content, ['openid'], true); $hook_data = Api\Hooks::process(array('location' => 'preferences_security')+$content, ['preferences', 'openid'], true);
foreach($hook_data as $extra_tabs) foreach($hook_data as $extra_tabs)
{ {
if (!$extra_tabs) continue; if (!$extra_tabs) continue;

View File

@ -7,6 +7,9 @@
*/ */
import {EgwApp} from '../../api/js/jsapi/egw_app'; import {EgwApp} from '../../api/js/jsapi/egw_app';
import type {Et2Button} from "../../api/js/etemplate/Et2Button/Et2Button";
import {Et2Dialog} from "../../api/js/etemplate/Et2Dialog/Et2Dialog";
import {egw} from "../../api/js/jsapi/egw_global";
/** /**
* JavaScript for Preferences * JavaScript for Preferences
@ -38,6 +41,20 @@ export class PreferencesApp extends EgwApp
// call parent // call parent
super.et2_ready(et2, name); super.et2_ready(et2, name);
} }
addToken(_ev : PointerEvent, _button : Et2Button)
{
console.log('app.preferences.addToken', arguments);
this.dialogExec('preferences.EGroupware\\Preferences\\Token.edit');
}
editToken(_action, _selection)
{
console.log('app.preferences.editToken', arguments);
this.dialogExec('preferences.EGroupware\\Preferences\\Token.edit&token_id='+_selection[0].id.split('::')[1]);
}
} }
// @ts-ignore // @ts-ignore

View File

@ -29,6 +29,8 @@ $setup_info['preferences']['hooks']['admin'] = 'preferences_hooks::admin
$setup_info['preferences']['hooks']['deny_prefs'] = 'preferences_hooks::deny_prefs'; $setup_info['preferences']['hooks']['deny_prefs'] = 'preferences_hooks::deny_prefs';
$setup_info['preferences']['hooks']['deny_acl'] = 'preferences_hooks::deny_acl'; $setup_info['preferences']['hooks']['deny_acl'] = 'preferences_hooks::deny_acl';
$setup_info['preferences']['hooks']['deny_cats'] = 'preferences_hooks::deny_cats'; $setup_info['preferences']['hooks']['deny_cats'] = 'preferences_hooks::deny_cats';
// Token / application passwords GUI for regular users
$setup_info['preferences']['hooks']['preferences_security'] = \EGroupware\Preferences\Token::class.'::security';
/* Dependencies for this app to work */ /* Dependencies for this app to work */
$setup_info['preferences']['depends'][] = array( $setup_info['preferences']['depends'][] = array(

57
preferences/src/Token.php Normal file
View File

@ -0,0 +1,57 @@
<?php
/**
* EGroupware - Admin - Application passwords / tokens
*
* @link https://www.egroupware.org
* @author Ralf Becker <rb-AT-egroupware.org>
* @package admin
* @copyright (c) 2023 by Ralf Becker <rb-AT-egroupware.org>
* @license https://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
*/
namespace EGroupware\Preferences;
use EGroupware\Api;
use EGroupware\Admin;
class Token extends Admin\Token
{
const APP = 'preferences';
/**
* Methods callable via menuaction GET parameter
*
* @var array
*/
public $public_functions = [
'edit' => true,
];
/**
* Answers preferences_password_security hook
*
* @param array $data
*/
public static function security(array $data)
{
Api\Translation::add_app('admin');
return [
'label' => 'Application passwords',
'name' => 'admin.tokens',
'prepend' => false,
'data' => [
'token' => [
'default_cols' => '!account_id',
'add_action' => 'app.preferences.addToken',
]+self::get_nm_options(),
],
'preserve' => [
],
'sel_options' => [
],
'save_callback' => __CLASS__.'::action',
];
}
}

View File

@ -113,3 +113,14 @@ img.qrCode {
table.prefTable tbody tr.prefRow .prefHelpColumn { table.prefTable tbody tr.prefRow .prefHelpColumn {
border-bottom: none; border-bottom: none;
} }
/**
* Application passwords / tokens
*/
tr.revoked > td * {
color: grey !important;
font-style: italic;
}
td.token {
border: 3px solid red;
}