Improved ACL handling #157

This commit is contained in:
the-djmaze 2024-02-13 10:44:38 +01:00
parent 7e304daee0
commit 306d3ec7b5
8 changed files with 110 additions and 116 deletions

View file

@ -402,8 +402,6 @@ export class FolderModel extends AbstractModel {
} }
), ),
canBeEdited: () => !this.type() && this.exists/* && this.selectable()*/,
isSystemFolder: () => this.type() isSystemFolder: () => this.type()
| (FolderUserStore.allowKolab() && !!this.kolabType() & !SettingsUserStore.unhideKolabFolders()), | (FolderUserStore.allowKolab() && !!this.kolabType() & !SettingsUserStore.unhideKolabFolders()),
@ -484,8 +482,7 @@ export class FolderModel extends AbstractModel {
} }
edit() { edit() {
// this.canBeEdited() && this.editing(true); showScreenPopup(FolderPopupView, [this]);
this.canBeEdited() && showScreenPopup(FolderPopupView, [this]);
} }
unedit() { unedit() {

View file

@ -15,6 +15,7 @@
} }
.folder-name { .folder-name {
cursor: pointer;
word-break: break-all; word-break: break-all;
white-space: pre-wrap; white-space: pre-wrap;
} }
@ -24,10 +25,6 @@
opacity: 0.6; opacity: 0.6;
} }
.folder-name.can-be-edited {
cursor: pointer;
}
select { select {
width: auto; width: auto;
} }

View file

@ -1,8 +1,7 @@
import { addObservablesTo } from 'External/ko';
import { AbstractViewPopup } from 'Knoin/AbstractViews'; import { AbstractViewPopup } from 'Knoin/AbstractViews';
import { addObservablesTo } from 'External/ko';
import Remote from 'Remote/User/Fetch'; import Remote from 'Remote/User/Fetch';
import { FolderUserStore } from 'Stores/User/Folder';
export class FolderPopupView extends AbstractViewPopup { export class FolderPopupView extends AbstractViewPopup {
constructor() { constructor() {
@ -10,7 +9,7 @@ export class FolderPopupView extends AbstractViewPopup {
addObservablesTo(this, { addObservablesTo(this, {
folder: null // FolderModel folder: null // FolderModel
}); });
this.ACLAllowed = FolderUserStore.hasCapability('ACL');
this.ACL = ko.observableArray(); this.ACL = ko.observableArray();
} }
@ -26,19 +25,14 @@ export class FolderPopupView extends AbstractViewPopup {
beforeShow(folder) { beforeShow(folder) {
this.ACL([]); this.ACL([]);
folder.editing(true); this.ACLAllowed && Remote.request('FolderACL', (iError, data) => {
this.folder(folder);
Remote.request('FolderACL', (iError, data) => {
if (!iError && data.Result) { if (!iError && data.Result) {
this.ACL( this.ACL(Object.values(data.Result));
Object.entries(data.Result).map(([key, value]) => {
value.identifier = key;
return value;
})
);
} }
}, { }, {
folder: folder.fullName folder: folder.fullName
}); });
!folder.type() && folder.exists && folder.selectable() && folder.editing(true);
this.folder(folder);
} }
} }

View file

@ -26,68 +26,82 @@ trait ACL
*/ */
public function ACLAllow(string $sFolderName, string $command) : bool public function ACLAllow(string $sFolderName, string $command) : bool
{ {
if ($this->ACLDisabled) { if ($this->ACLDisabled || !$this->hasCapability('ACL')) {
return false; return false;
} }
// The "RIGHTS=" capability MUST NOT include any of the rights defined in RFC 2086: // The "RIGHTS=" capability MUST NOT include any of the rights defined in RFC 2086:
// "l", "r", "s", "w", "i", "p", "a", "c", "d", and the digits ("0" .. "9") // "l", "r", "s", "w", "i", "p", "a", "c", "d", and the digits ("0" .. "9")
// So it is: RIGHTS=texk // So it is: RIGHTS=texk
$mainRights = \str_split($this->CapabilityValue('RIGHTS') ?: ''); // $mainRights = \str_split($this->CapabilityValue('RIGHTS') ?: '');
if ($this->hasCapability('ACL') || $mainRights) {
if ('MYRIGHTS' === $command) { if ('MYRIGHTS' === $command) {
// at least one of the "l", "r", "i", "k", "x", "a" rights is required // at least one of the "l", "r", "i", "k", "x", "a" rights is required
return true; return true;
} }
$rights = $this->FolderMyRights($sFolderName);
if ($rights) { if (\in_array($command, ['GETACL','SETACL','LISTRIGHTS','DELETEACL'])) {
switch ($command) return true;
{ }
case 'LIST':
case 'LSUB': $rights = $this->FolderMyRights($sFolderName);
return $rights->hasRight('LOOKUP'); if ($rights) {
case 'CREATE': switch ($command)
return true; // $parent->$rights->hasRight('k'); {
case 'DELETE': case 'LIST':
return $rights->hasRight('x'); case 'LSUB':
case 'RENAME': return $rights->hasRight('LOOKUP');
return $rights->hasRight('k') && $rights->hasRight('x'); case 'CREATE':
case 'SELECT': return true; // $parent->$rights->hasRight('k');
case 'EXAMINE': case 'DELETE':
case 'STATUS': return $rights->hasRight('x');
return $rights->hasRight('r'); case 'RENAME':
case 'APPEND': return $rights->hasRight('k') && $rights->hasRight('x');
case 'COPY': case 'SELECT':
return $rights->hasRight('i'); case 'EXAMINE':
case 'EXPUNGE': case 'STATUS':
return $rights->hasRight('e'); return $rights->hasRight('r');
case 'APPEND':
case 'COPY':
return $rights->hasRight('i');
case 'EXPUNGE':
return $rights->hasRight('e');
/* /*
case 'SUBSCRIBE': case 'SUBSCRIBE':
return $rights->hasRight('l') || true; return $rights->hasRight('l') || true;
case 'UNSUBSCRIBE': case 'UNSUBSCRIBE':
return true; return true;
case 'CLOSE': case 'CLOSE':
return $rights->hasRight('e') || true; return $rights->hasRight('e') || true;
case 'FETCH': case 'FETCH':
return $rights->hasRight('s') || true; return $rights->hasRight('s') || true;
case 'STORE': case 'STORE':
return $rights->hasRight('s') || $rights->hasRight('w') || $rights->hasRight('t'); return $rights->hasRight('s') || $rights->hasRight('w') || $rights->hasRight('t');
*/ */
case 'GETACL': case 'GETACL':
case 'SETACL': case 'SETACL':
case 'LISTRIGHTS': case 'LISTRIGHTS':
case 'DELETEACL': case 'DELETEACL':
return $rights->hasRight('a'); return $rights->hasRight('a');
}
} }
} }
return !\in_array($command, ['GETACL','SETACL','LISTRIGHTS','DELETEACL','MYRIGHTS']); return true;
}
private function FolderACLRequest(string $sFolderName, string $sCommand, array $aParams) : \MailSo\Imap\ResponseCollection
{
if ($this->ACLAllow($sFolderName, $sCommand)) try {
return $this->SendRequestGetResponse($sCommand, $aParams);
} catch (\Throwable $oException) {
// Error in IMAP command $sCommand: ACLs disabled
$this->ACLDisabled = true;
throw $oException;
}
} }
public function FolderSetACL(string $sFolderName, string $sIdentifier, string $sAccessRights) : void public function FolderSetACL(string $sFolderName, string $sIdentifier, string $sAccessRights) : void
{ {
// if ($this->ACLAllow($sFolderName, 'SETACL')) { $this->FolderACLRequest($sFolderName, 'SETACL', array(
$this->SendRequestGetResponse('SETACL', array(
$this->EscapeFolderName($sFolderName), $this->EscapeFolderName($sFolderName),
$this->EscapeString($sIdentifier), $this->EscapeString($sIdentifier),
$this->EscapeString($sAccessRights) $this->EscapeString($sAccessRights)
@ -96,8 +110,7 @@ trait ACL
public function FolderDeleteACL(string $sFolderName, string $sIdentifier) : void public function FolderDeleteACL(string $sFolderName, string $sIdentifier) : void
{ {
// if ($this->ACLAllow($sFolderName, 'DELETEACL')) { $this->FolderACLRequest($sFolderName, 'DELETEACL', array(
$this->SendRequestGetResponse('DELETEACL', array(
$this->EscapeFolderName($sFolderName), $this->EscapeFolderName($sFolderName),
$this->EscapeString($sIdentifier) $this->EscapeString($sIdentifier)
)); ));
@ -105,9 +118,8 @@ trait ACL
public function FolderGetACL(string $sFolderName) : array public function FolderGetACL(string $sFolderName) : array
{ {
// if ($this->ACLAllow($sFolderName, 'GETACL')) {
$oResponses = $this->SendRequestGetResponse('GETACL', array($this->EscapeFolderName($sFolderName)));
$aResult = array(); $aResult = array();
$oResponses = $this->FolderACLRequest($sFolderName, 'GETACL', array($this->EscapeFolderName($sFolderName)));
foreach ($oResponses as $oResponse) { foreach ($oResponses as $oResponse) {
// * ACL INBOX.shared demo@snappymail.eu akxeilprwtscd foobar@snappymail.eu akxeilprwtscd demo2@snappymail.eu lrwstipekxacd // * ACL INBOX.shared demo@snappymail.eu akxeilprwtscd foobar@snappymail.eu akxeilprwtscd demo2@snappymail.eu lrwstipekxacd
if (\MailSo\Imap\Enumerations\ResponseType::UNTAGGED === $oResponse->ResponseType if (\MailSo\Imap\Enumerations\ResponseType::UNTAGGED === $oResponse->ResponseType
@ -118,7 +130,10 @@ trait ACL
{ {
$c = \count($oResponse->ResponseList); $c = \count($oResponse->ResponseList);
for ($i = 3; $i < $c; $i += 2) { for ($i = 3; $i < $c; $i += 2) {
$aResult[$oResponse->ResponseList[$i]] = new ACLResponse(\str_split($oResponse->ResponseList[$i+1])); $aResult[] = new ACLResponse(
$oResponse->ResponseList[$i],
$oResponse->ResponseList[$i+1]
);
} }
} }
} }
@ -127,8 +142,7 @@ trait ACL
public function FolderListRights(string $sFolderName, string $sIdentifier) : ?ACLResponse public function FolderListRights(string $sFolderName, string $sIdentifier) : ?ACLResponse
{ {
// if ($this->ACLAllow($sFolderName, 'LISTRIGHTS')) { $oResponses = $this->FolderACLRequest($sFolderName, 'LISTRIGHTS', array(
$oResponses = $this->SendRequestGetResponse('LISTRIGHTS', array(
$this->EscapeFolderName($sFolderName), $this->EscapeFolderName($sFolderName),
$this->EscapeString($sIdentifier) $this->EscapeString($sIdentifier)
)); ));
@ -140,7 +154,10 @@ trait ACL
&& $sIdentifier === $oResponse->ResponseList[3] && $sIdentifier === $oResponse->ResponseList[3]
) )
{ {
return static::aclRightsToClass(\array_slice($oResponse->ResponseList, 4)); foreach (\array_slice($oResponse->ResponseList, 4) as $rule) {
$result = \array_merge($result, \str_split($rule));
}
return new ACLResponse($sIdentifier, \implode('', \array_unique($result)));
} }
} }
return null; return null;
@ -148,35 +165,17 @@ trait ACL
public function FolderMyRights(string $sFolderName) : ?ACLResponse public function FolderMyRights(string $sFolderName) : ?ACLResponse
{ {
// if ($this->ACLAllow($sFolderName, 'MYRIGHTS')) { $oResponses = $this->FolderACLRequest($sFolderName, 'MYRIGHTS', array($this->EscapeFolderName($sFolderName)));
// if ($this->hasCapability('ACL')) { foreach ($oResponses as $oResponse) {
try { if (\MailSo\Imap\Enumerations\ResponseType::UNTAGGED === $oResponse->ResponseType
$oResponses = $this->SendRequestGetResponse('MYRIGHTS', array($this->EscapeFolderName($sFolderName))); && isset($oResponse->ResponseList[3])
foreach ($oResponses as $oResponse) { && 'MYRIGHTS' === $oResponse->ResponseList[1]
if (\MailSo\Imap\Enumerations\ResponseType::UNTAGGED === $oResponse->ResponseType && $sFolderName === $oResponse->ResponseList[2]
&& isset($oResponse->ResponseList[3]) )
&& 'MYRIGHTS' === $oResponse->ResponseList[1] {
&& $sFolderName === $oResponse->ResponseList[2] return new ACLResponse('', $oResponse->ResponseList[3]);
)
{
return static::aclRightsToClass(\array_slice($oResponse->ResponseList, 3));
}
} }
} catch (\Throwable $oException) {
// \MailSo\Imap\Exceptions\NegativeResponseException: Error in IMAP command MYRIGHTS: ACLs disabled
$this->ACLDisabled = true;
throw $oException;
} }
return null; return null;
} }
private static function aclRightsToClass(array $rules) : ACLResponse
{
$result = array();
foreach ($rules as $rule) {
$result = \array_merge($result, \str_split($rule));
}
return new ACLResponse(\array_unique($result));
}
} }

View file

@ -576,7 +576,7 @@ trait Folders
} }
/* /*
// RFC 4314 // RFC 4314
if ($this->hasCapability('ACL') || $this->CapabilityValue('RIGHTS')) { if ($this->hasCapability('ACL')) {
foreach ($oFolderCollection as $oFolder) { foreach ($oFolderCollection as $oFolder) {
if ($oFolder->Selectable()) try { if ($oFolder->Selectable()) try {
$oFolder->myRights = $this->FolderMyRights($oFolder->FullName); $oFolder->myRights = $this->FolderMyRights($oFolder->FullName);

View file

@ -17,18 +17,26 @@ namespace MailSo\Imap\Responses;
*/ */
class ACL implements \JsonSerializable class ACL implements \JsonSerializable
{ {
private array $rights; private string
$identifier,
$rights;
function __construct(array $rights) function __construct(string $identifier, string $rights)
{ {
$this->identifier = $identifier;
$this->rights = $rights; $this->rights = $rights;
} }
public function identifier() : string
{
return $this->identifier;
}
/** PHP 8.1 /** PHP 8.1
public function hasRight(string|\MailSo\Imap\Enumerations\FolderACL $right) public function hasRight(string|\MailSo\Imap\Enumerations\FolderACL $right)
{ {
if ($right instanceof \BackedEnum) { if ($right instanceof \BackedEnum) {
return \in_array($right->value, $this->rights); return \str_contains($this->rights, $right->value);
} }
*/ */
public function hasRight(string $right) : bool public function hasRight(string $right) : bool
@ -37,13 +45,15 @@ class ACL implements \JsonSerializable
if (\defined($const)) { if (\defined($const)) {
$right = \constant($const); $right = \constant($const);
} }
return \in_array($right, $this->rights); return \str_contains($this->rights, $right);
} }
#[\ReturnTypeWillChange] #[\ReturnTypeWillChange]
public function jsonSerialize() public function jsonSerialize()
{ {
return [ return [
'identifier' => $this->identifier,
'rights' => $this->rights,
'mayReadItems' => ($this->hasRight('l') && $this->hasRight('r')), 'mayReadItems' => ($this->hasRight('l') && $this->hasRight('r')),
'mayAddItems' => $this->hasRight('i'), 'mayAddItems' => $this->hasRight('i'),
'mayRemoveItems' => ($this->hasRight('t') && $this->hasRight('e')), 'mayRemoveItems' => ($this->hasRight('t') && $this->hasRight('e')),
@ -52,8 +62,7 @@ class ACL implements \JsonSerializable
'mayCreateChild' => $this->hasRight('k'), 'mayCreateChild' => $this->hasRight('k'),
'mayRename' => $this->hasRight('x'), 'mayRename' => $this->hasRight('x'),
'mayDelete' => $this->hasRight('x'), 'mayDelete' => $this->hasRight('x'),
'maySubmit' => $this->hasRight('p'), 'maySubmit' => $this->hasRight('p')
'raw' => \implode('', $this->rights)
]; ];
} }

View file

@ -4,7 +4,7 @@
</header> </header>
<form id="folderform" class="modal-body form-horizontal" autocomplete="off" spellcheck="false" data-bind="submit: submitForm"> <form id="folderform" class="modal-body form-horizontal" autocomplete="off" spellcheck="false" data-bind="submit: submitForm">
<div data-bind="with: folder"> <div data-bind="with: folder">
<div class="control-group"> <div class="control-group" data-bind="if: editing">
<label data-i18n="GLOBAL/NAME"></label> <label data-i18n="GLOBAL/NAME"></label>
<input name="folder" type="text" <input name="folder" type="text"
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
@ -30,7 +30,7 @@
}"></div> }"></div>
<!-- /ko --> <!-- /ko -->
</div> </div>
<div class="control-group" data-bind="if: ACL"> <div class="control-group" data-bind="if: ACLAllowed">
<label>ACL</label> <label>ACL</label>
<div data-bind="foreach: ACL"> <div data-bind="foreach: ACL">
<details> <details>

View file

@ -1,8 +1,6 @@
<tr data-bind="css: { 'selectable': selectable, 'system': isSystemFolder }"> <tr data-bind="css: { 'selectable': selectable, 'system': isSystemFolder }">
<td data-bind="css: 'deep-' + deep"> <td data-bind="css: 'deep-' + deep">
<span class="folder-name" data-bind="text: name, css: { 'can-be-edited': canBeEdited }, <span class="folder-name" data-bind="text: name, click: edit, tooltipErrorTip: errorMsg"></span>
click: edit,
tooltipErrorTip: errorMsg"></span>
<span class="folder-system-name" data-bind="text: nameInfo, visible: isSystemFolder"></span> <span class="folder-system-name" data-bind="text: nameInfo, visible: isSystemFolder"></span>
<span class="folder-size" data-bind="text: friendlySize, visible: size"></span> <span class="folder-size" data-bind="text: friendlySize, visible: size"></span>
</td> </td>