Merge pull request #10836 from nupplaphil/feat/depository_permissionset

Migrate PermissionSet to Depository paradigm
This commit is contained in:
Hypolite Petovan 2021-10-08 09:39:39 -04:00 committed by GitHub
commit 80a8cd86c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 634 additions and 394 deletions

View File

@ -13,7 +13,7 @@ use Psr\Log\LoggerInterface;
* Depositories are meant to store and retrieve Entities from the database. * Depositories are meant to store and retrieve Entities from the database.
* *
* The reason why there are methods prefixed with an underscore is because PHP doesn't support generic polymorphism * The reason why there are methods prefixed with an underscore is because PHP doesn't support generic polymorphism
* which means we can't direcly overload base methods and make parameters more strict (from a parent class to a child * which means we can't directly overload base methods and make parameters more strict (from a parent class to a child
* class for example) * class for example)
* *
* Similarly, we can't make an overloaded method return type more strict until we only support PHP version 7.4 but this * Similarly, we can't make an overloaded method return type more strict until we only support PHP version 7.4 but this

View File

@ -7,7 +7,7 @@ use Friendica\BaseEntity;
interface ICanCreateFromTableRow interface ICanCreateFromTableRow
{ {
/** /**
* Returns the correcponding Entity given a table row record * Returns the corresponding Entity given a table row record
* *
* @param array $row * @param array $row
* @return BaseEntity * @return BaseEntity

View File

@ -1,29 +0,0 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Collection;
use Friendica\BaseCollection;
class PermissionSets extends BaseCollection
{
}

View File

@ -21,14 +21,12 @@
namespace Friendica\Core; namespace Friendica\Core;
use Friendica\App;
use Friendica\Database\DBA; use Friendica\Database\DBA;
use Friendica\Database\DBStructure; use Friendica\Database\DBStructure;
use Friendica\DI; use Friendica\DI;
use Friendica\Model\Contact;
use Friendica\Model\Photo; use Friendica\Model\Photo;
use Friendica\Object\Image; use Friendica\Object\Image;
use Friendica\Repository\PermissionSet; use Friendica\Security\PermissionSet\Depository\PermissionSet;
use Friendica\Util\Strings; use Friendica\Util\Strings;
use Friendica\Worker\Delivery; use Friendica\Worker\Delivery;
@ -283,16 +281,13 @@ class UserImport
DI::profileField()->migrateFromLegacyProfile($profile); DI::profileField()->migrateFromLegacyProfile($profile);
} }
///@TODO Replace with permissionset import $permissionSet = DI::permissionSet()->selectDefaultForUser($newuid);
$self_contact = Contact::selectFirst(['id'], ['uid' => $newuid, 'self' => true]);
$allow_cid = DI::aclFormatter()->toString($self_contact['id']);
$self_psid = DI::permissionSet()->getIdFromACL($newuid, $allow_cid);
foreach ($account['profile_fields'] ?? [] as $profile_field) { foreach ($account['profile_fields'] ?? [] as $profile_field) {
$profile_field['uid'] = $newuid; $profile_field['uid'] = $newuid;
///@TODO Replace with permissionset import ///@TODO Replace with permissionset import
$profile_field['psid'] = $profile_field['psid'] ? $self_psid : PermissionSet::PUBLIC; $profile_field['psid'] = $profile_field['psid'] ? $permissionSet->uid : PermissionSet::PUBLIC;
if (self::dbImportAssoc('profile_field', $profile_field) === false) { if (self::dbImportAssoc('profile_field', $profile_field) === false) {
Logger::info("uimport:insert profile field " . $profile_field['id'] . " : ERROR : " . DBA::errorMessage()); Logger::info("uimport:insert profile field " . $profile_field['id'] . " : ERROR : " . DBA::errorMessage());

View File

@ -442,12 +442,14 @@ abstract class DI
return self::$dice->create(Repository\Introduction::class); return self::$dice->create(Repository\Introduction::class);
} }
/** public static function permissionSet(): Security\PermissionSet\Depository\PermissionSet
* @return Repository\PermissionSet
*/
public static function permissionSet()
{ {
return self::$dice->create(Repository\PermissionSet::class); return self::$dice->create(Security\PermissionSet\Depository\PermissionSet::class);
}
public static function permissionSetFactory(): Security\PermissionSet\Factory\PermissionSet
{
return self::$dice->create(Security\PermissionSet\Factory\PermissionSet::class);
} }
/** /**

View File

@ -27,8 +27,8 @@ use Friendica\Collection\Api\Mastodon\Fields;
use Friendica\Model\APContact; use Friendica\Model\APContact;
use Friendica\Model\Contact; use Friendica\Model\Contact;
use Friendica\Network\HTTPException; use Friendica\Network\HTTPException;
use Friendica\Repository\PermissionSet;
use Friendica\Repository\ProfileField; use Friendica\Repository\ProfileField;
use Friendica\Security\PermissionSet\Depository\PermissionSet;
use ImagickException; use ImagickException;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;

View File

@ -980,13 +980,14 @@ class Item
} }
// Creates or assigns the permission set // Creates or assigns the permission set
$item['psid'] = PermissionSet::getIdFromACL( $item['psid'] = DI::permissionSet()->selectOrCreate(
DI::permissionSetFactory()->createFromString(
$item['uid'], $item['uid'],
$item['allow_cid'], $item['allow_cid'],
$item['allow_gid'], $item['allow_gid'],
$item['deny_cid'], $item['deny_cid'],
$item['deny_gid'] $item['deny_gid']
); ))->id;
if (!empty($item['extid'])) { if (!empty($item['extid'])) {
$item['external-id'] = ItemURI::getIdByURI($item['extid']); $item['external-id'] = ItemURI::getIdByURI($item['extid']);
@ -1952,18 +1953,19 @@ class Item
$private = self::PUBLIC; $private = self::PUBLIC;
} }
$psid = PermissionSet::getIdFromACL( $permissionSet = DI::permissionSet()->selectOrCreate(
DI::permissionSetFactory()->createFromString(
$user['uid'], $user['uid'],
$user['allow_cid'], $user['allow_cid'],
$user['allow_gid'], $user['allow_gid'],
$user['deny_cid'], $user['deny_cid'],
$user['deny_gid'] $user['deny_gid']
); ));
$forum_mode = ($prvgroup ? 2 : 1); $forum_mode = ($prvgroup ? 2 : 1);
$fields = ['wall' => true, 'origin' => true, 'forum_mode' => $forum_mode, 'contact-id' => $self['id'], $fields = ['wall' => true, 'origin' => true, 'forum_mode' => $forum_mode, 'contact-id' => $self['id'],
'owner-id' => $owner_id, 'private' => $private, 'psid' => $psid]; 'owner-id' => $owner_id, 'private' => $private, 'psid' => $permissionSet->id];
self::update($fields, ['id' => $item['id']]); self::update($fields, ['id' => $item['id']]);
Worker::add(['priority' => PRIORITY_HIGH, 'dont_fork' => true], 'Notifier', Delivery::POST, (int)$item['uri-id'], (int)$item['uid']); Worker::add(['priority' => PRIORITY_HIGH, 'dont_fork' => true], 'Notifier', Delivery::POST, (int)$item['uri-id'], (int)$item['uid']);
@ -2549,12 +2551,12 @@ class Item
$condition = []; $condition = [];
} elseif ($remote_user) { } elseif ($remote_user) {
// Authenticated visitor - fetch the matching permissionsets // Authenticated visitor - fetch the matching permissionsets
$set = PermissionSet::get($owner_id, $remote_user); $permissionSets = DI::permissionSet()->selectByContactId($remote_user, $owner_id);
if (!empty($set)) { if (!empty($set)) {
$condition = ["(`private` != ? OR (`private` = ? AND `wall` $condition = ["(`private` != ? OR (`private` = ? AND `wall`
AND `psid` IN (" . implode(', ', array_fill(0, count($set), '?')) . ")))", AND `psid` IN (" . implode(', ', array_fill(0, count($set), '?')) . ")))",
self::PRIVATE, self::PRIVATE]; self::PRIVATE, self::PRIVATE];
$condition = array_merge($condition, $set); $condition = array_merge($condition, $permissionSets->column('id'));
} }
} }
@ -2595,10 +2597,10 @@ class Item
* If pre-verified, the caller is expected to have already * If pre-verified, the caller is expected to have already
* done this and passed the groups into this function. * done this and passed the groups into this function.
*/ */
$set = PermissionSet::get($owner_id, $remote_user); $permissionSets = DI::permissionSet()->selectByContactId($remote_user, $owner_id);
if (!empty($set)) { if (!empty($set)) {
$sql_set = sprintf(" OR (" . $table . "`private` = %d AND " . $table . "`wall` AND " . $table . "`psid` IN (", self::PRIVATE) . implode(',', $set) . "))"; $sql_set = sprintf(" OR (" . $table . "`private` = %d AND " . $table . "`wall` AND " . $table . "`psid` IN (", self::PRIVATE) . implode(',', $permissionSets->column('id')) . "))";
} else { } else {
$sql_set = ''; $sql_set = '';
} }

View File

@ -1,78 +0,0 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Model;
use Friendica\BaseModel;
use Friendica\DI;
/**
* functions for interacting with the permission set of an object (item, photo, event, ...)
*
* @property int uid
* @property string allow_cid
* @property string allow_gid
* @property string deny_cid
* @property string deny_gid
*/
class PermissionSet extends BaseModel
{
/**
* Fetch the id of a given permission set. Generate a new one when needed
*
* @param int $uid
* @param string|null $allow_cid Allowed contact IDs - empty = everyone
* @param string|null $allow_gid Allowed group IDs - empty = everyone
* @param string|null $deny_cid Disallowed contact IDs - empty = no one
* @param string|null $deny_gid Disallowed group IDs - empty = no one
* @return int id
* @throws \Exception
* @deprecated since 2020.03, use Repository\PermissionSet instead
* @see \Friendica\Repository\PermissionSet->getIdFromACL
*/
public static function getIdFromACL(
int $uid,
string $allow_cid = null,
string $allow_gid = null,
string $deny_cid = null,
string $deny_gid = null
) {
return DI::permissionSet()->getIdFromACL($uid, $allow_cid, $allow_gid, $deny_cid, $deny_gid);
}
/**
* Returns a permission set for a given contact
*
* @param integer $uid User id whom the items belong
* @param integer $contact_id Contact id of the visitor
*
* @return array of permission set ids.
* @throws \Exception
* @deprecated since 2020.03, use Repository\PermissionSet instead
* @see \Friendica\Repository\PermissionSet->selectByContactId
*/
public static function get($uid, $contact_id)
{
$permissionSets = DI::permissionSet()->selectByContactId($contact_id, $uid);
return $permissionSets->column('id');
}
}

View File

@ -23,7 +23,9 @@ namespace Friendica\Model;
use Friendica\BaseModel; use Friendica\BaseModel;
use Friendica\Database\Database; use Friendica\Database\Database;
use Friendica\Network\HTTPException; use Friendica\Network\HTTPException\NotFoundException;
use Friendica\Security\PermissionSet\Depository\PermissionSet as PermissionSetDepository;
use Friendica\Security\PermissionSet\Entity\PermissionSet;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
/** /**
@ -39,21 +41,21 @@ use Psr\Log\LoggerInterface;
* @property string value * @property string value
* @property string created * @property string created
* @property string edited * @property string edited
* @property PermissionSet permissionset * @property PermissionSet permissionSet
*/ */
class ProfileField extends BaseModel class ProfileField extends BaseModel
{ {
/** @var PermissionSet */ /** @var PermissionSet */
private $permissionset; private $permissionSet;
/** @var \Friendica\Repository\PermissionSet */ /** @var PermissionSetDepository */
private $permissionSetRepository; private $permissionSetDepository;
public function __construct(Database $dba, LoggerInterface $logger, \Friendica\Repository\PermissionSet $permissionSetRepository, array $data = []) public function __construct(Database $dba, LoggerInterface $logger, PermissionSetDepository $permissionSetDepository, array $data = [])
{ {
parent::__construct($dba, $logger, $data); parent::__construct($dba, $logger, $data);
$this->permissionSetRepository = $permissionSetRepository; $this->permissionSetDepository = $permissionSetDepository;
} }
public function __get($name) public function __get($name)
@ -61,12 +63,17 @@ class ProfileField extends BaseModel
$this->checkValid(); $this->checkValid();
switch ($name) { switch ($name) {
case 'permissionset': case 'permissionSet':
$this->permissionset = if (empty($this->permissionSet)) {
$this->permissionset ?? $permissionSet = $this->permissionSetDepository->selectOneById($this->psid);
$this->permissionSetRepository->selectFirst(['id' => $this->psid, 'uid' => $this->uid]); if ($permissionSet->uid !== $this->uid) {
throw new NotFoundException(sprintf('PermissionSet %d (user-id: %d) for ProfileField %d (user-id: %d) is invalid.', $permissionSet->id, $permissionSet->uid, $this->id, $this->uid));
}
$return = $this->permissionset; $this->permissionSet = $permissionSet;
}
$return = $this->permissionSet;
break; break;
default: default:
$return = parent::__get($name); $return = parent::__get($name);

View File

@ -86,7 +86,7 @@ class Objects extends BaseModule
$permissionSets = DI::permissionSet()->selectByContactId($requester_id, $item['uid']); $permissionSets = DI::permissionSet()->selectByContactId($requester_id, $item['uid']);
if (!empty($permissionSets)) { if (!empty($permissionSets)) {
$psid = array_merge($permissionSets->column('id'), $psid = array_merge($permissionSets->column('id'),
[DI::permissionSet()->getIdFromACL($item['uid'], '', '', '', '')]); [DI::permissionSet()->selectEmptyForUser($item['uid'])]);
$validated = in_array($item['psid'], $psid); $validated = in_array($item['psid'], $psid);
} }
} }

View File

@ -28,7 +28,7 @@ use Friendica\Model\Contact;
use Friendica\Model\Profile; use Friendica\Model\Profile;
use Friendica\Module\BaseApi; use Friendica\Module\BaseApi;
use Friendica\Network\HTTPException; use Friendica\Network\HTTPException;
use Friendica\Repository\PermissionSet; use Friendica\Security\PermissionSet\Depository\PermissionSet;
/** /**
* API endpoint: /api/friendica/profile/show * API endpoint: /api/friendica/profile/show

View File

@ -39,7 +39,7 @@ class PermissionTooltip extends \Friendica\BaseModule
} }
if (isset($model['psid'])) { if (isset($model['psid'])) {
$permissionSet = DI::permissionSet()->selectFirst(['id' => $model['psid']]); $permissionSet = DI::permissionSet()->selectOneById($model['psid']);
$model['allow_cid'] = $permissionSet->allow_cid; $model['allow_cid'] = $permissionSet->allow_cid;
$model['allow_gid'] = $permissionSet->allow_gid; $model['allow_gid'] = $permissionSet->allow_gid;
$model['deny_cid'] = $permissionSet->deny_cid; $model['deny_cid'] = $permissionSet->deny_cid;
@ -61,12 +61,10 @@ class PermissionTooltip extends \Friendica\BaseModule
exit; exit;
} }
$aclFormatter = DI::aclFormatter(); $allowed_users = $model['allow_cid'];
$allowed_groups = $model['allow_gid'];
$allowed_users = $aclFormatter->expand($model['allow_cid']); $deny_users = $model['deny_cid'];
$allowed_groups = $aclFormatter->expand($model['allow_gid']); $deny_groups = $model['deny_gid'];
$deny_users = $aclFormatter->expand($model['deny_cid']);
$deny_groups = $aclFormatter->expand($model['deny_gid']);
$o = DI::l10n()->t('Visible to:') . '<br />'; $o = DI::l10n()->t('Visible to:') . '<br />';
$l = []; $l = [];

View File

@ -213,7 +213,7 @@ class Status extends BaseProfile
$permissionSets = DI::permissionSet()->selectByContactId($remote_user, $profile['uid']); $permissionSets = DI::permissionSet()->selectByContactId($remote_user, $profile['uid']);
if (!empty($permissionSets)) { if (!empty($permissionSets)) {
$condition = ['psid' => array_merge($permissionSets->column('id'), $condition = ['psid' => array_merge($permissionSets->column('id'),
[DI::permissionSet()->getIdFromACL($profile['uid'], '', '', '', '')])]; [DI::permissionSet()->selectEmptyForUser($profile['uid'])])];
} }
} elseif ($profile['uid'] == local_user()) { } elseif ($profile['uid'] == local_user()) {
$condition = []; $condition = [];

View File

@ -28,6 +28,7 @@ use Friendica\Core\Renderer;
use Friendica\Core\Theme; use Friendica\Core\Theme;
use Friendica\Database\DBA; use Friendica\Database\DBA;
use Friendica\DI; use Friendica\DI;
use Friendica\Model\Contact;
use Friendica\Model\Profile; use Friendica\Model\Profile;
use Friendica\Model\ProfileField; use Friendica\Model\ProfileField;
use Friendica\Model\User; use Friendica\Model\User;
@ -161,7 +162,9 @@ class Index extends BaseSettings
$profileFields = DI::profileField()->selectByUserId(local_user()); $profileFields = DI::profileField()->selectByUserId(local_user());
foreach ($profileFields as $profileField) { foreach ($profileFields as $profileField) {
/** @var ProfileField $profileField */ /** @var ProfileField $profileField */
$defaultPermissions = ACL::getDefaultUserPermissions($profileField->permissionset->toArray()); $defaultPermissions = $profileField->permissionSet->withAllowedContacts(
Contact::pruneUnavailable($profileField->permissionSet->allow_cid)
);
$custom_fields[] = [ $custom_fields[] = [
'id' => $profileField->id, 'id' => $profileField->id,
@ -173,7 +176,7 @@ class Index extends BaseSettings
DI::page(), DI::page(),
$a->getLoggedInUserId(), $a->getLoggedInUserId(),
false, false,
$defaultPermissions, $defaultPermissions->toArray(),
['network' => Protocol::DFRN], ['network' => Protocol::DFRN],
'profile_field[' . $profileField->id . ']' 'profile_field[' . $profileField->id . ']'
), ),

View File

@ -242,7 +242,7 @@ class Transmitter
$permissionSets = DI::permissionSet()->selectByContactId($requester_id, $owner['uid']); $permissionSets = DI::permissionSet()->selectByContactId($requester_id, $owner['uid']);
if (!empty($permissionSets)) { if (!empty($permissionSets)) {
$condition = ['psid' => array_merge($permissionSets->column('id'), $condition = ['psid' => array_merge($permissionSets->column('id'),
[DI::permissionSet()->getIdFromACL($owner['uid'], '', '', '', '')])]; [DI::permissionSet()->selectEmptyForUser($owner['uid'])])];
} }
} }
} }

View File

@ -1,197 +0,0 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Repository;
use Friendica\BaseRepository;
use Friendica\Collection;
use Friendica\Database\Database;
use Friendica\Model;
use Friendica\Network\HTTPException;
use Friendica\Util\ACLFormatter;
use Psr\Log\LoggerInterface;
class PermissionSet extends BaseRepository
{
/** @var int Virtual permission set id for public permission */
const PUBLIC = 0;
protected static $table_name = 'permissionset';
protected static $model_class = Model\PermissionSet::class;
protected static $collection_class = Collection\PermissionSets::class;
/** @var ACLFormatter */
private $aclFormatter;
public function __construct(Database $dba, LoggerInterface $logger, ACLFormatter $aclFormatter)
{
parent::__construct($dba, $logger);
$this->aclFormatter = $aclFormatter;
}
/**
* @param array $data
* @return Model\PermissionSet
*/
protected function create(array $data)
{
return new Model\PermissionSet($this->dba, $this->logger, $data);
}
/**
* @param array $condition
* @return Model\PermissionSet
* @throws \Friendica\Network\HTTPException\NotFoundException
*/
public function selectFirst(array $condition)
{
if (isset($condition['id']) && !$condition['id']) {
return $this->create([
'id' => self::PUBLIC,
'uid' => $condition['uid'] ?? 0,
'allow_cid' => '',
'allow_gid' => '',
'deny_cid' => '',
'deny_gid' => '',
]);
}
return parent::selectFirst($condition);
}
/**
* @param array $condition
* @param array $params
* @return Collection\PermissionSets
* @throws \Exception
*/
public function select(array $condition = [], array $params = [])
{
return parent::select($condition, $params);
}
/**
* @param array $condition
* @param array $params
* @param int|null $min_id
* @param int|null $max_id
* @param int $limit
* @return Collection\PermissionSets
* @throws \Exception
*/
public function selectByBoundaries(array $condition = [], array $params = [], int $min_id = null, int $max_id = null, int $limit = self::LIMIT)
{
return parent::selectByBoundaries($condition, $params, $min_id, $max_id, $limit);
}
/**
* Fetch the id of a given permission set. Generate a new one when needed
*
* @param int $uid
* @param string|null $allow_cid Allowed contact IDs - empty = everyone
* @param string|null $allow_gid Allowed group IDs - empty = everyone
* @param string|null $deny_cid Disallowed contact IDs - empty = no one
* @param string|null $deny_gid Disallowed group IDs - empty = no one
* @return int id
* @throws \Exception
*/
public function getIdFromACL(
int $uid,
string $allow_cid = null,
string $allow_gid = null,
string $deny_cid = null,
string $deny_gid = null
) {
$allow_cid = $this->aclFormatter->sanitize($allow_cid);
$allow_gid = $this->aclFormatter->sanitize($allow_gid);
$deny_cid = $this->aclFormatter->sanitize($deny_cid);
$deny_gid = $this->aclFormatter->sanitize($deny_gid);
// Public permission
if (!$allow_cid && !$allow_gid && !$deny_cid && !$deny_gid) {
return self::PUBLIC;
}
$condition = [
'uid' => $uid,
'allow_cid' => $allow_cid,
'allow_gid' => $allow_gid,
'deny_cid' => $deny_cid,
'deny_gid' => $deny_gid
];
try {
$permissionset = $this->selectFirst($condition);
} catch(HTTPException\NotFoundException $exception) {
$permissionset = $this->insert($condition);
}
return $permissionset->id;
}
/**
* Returns a permission set collection for a given contact
*
* @param integer $contact_id Contact id of the visitor
* @param integer $uid User id whom the items belong, used for ownership check.
*
* @return Collection\PermissionSets
* @throws \Exception
*/
public function selectByContactId($contact_id, $uid)
{
$cdata = Model\Contact::getPublicAndUserContactID($contact_id, $uid);
if (!empty($cdata)) {
$public_contact_str = '<' . $cdata['public'] . '>';
$user_contact_str = '<' . $cdata['user'] . '>';
$contact_id = $cdata['user'];
} else {
$public_contact_str = '<' . $contact_id . '>';
$user_contact_str = '';
}
$groups = [];
if (!empty($user_contact_str) && $this->dba->exists('contact', ['id' => $contact_id, 'uid' => $uid, 'blocked' => false])) {
$groups = Model\Group::getIdsByContactId($contact_id);
}
$group_str = '<<>>'; // should be impossible to match
foreach ($groups as $group_id) {
$group_str .= '|<' . preg_quote($group_id) . '>';
}
if (!empty($user_contact_str)) {
$condition = ["`uid` = ? AND (NOT (`deny_cid` REGEXP ? OR `deny_cid` REGEXP ? OR deny_gid REGEXP ?)
AND (allow_cid REGEXP ? OR allow_cid REGEXP ? OR allow_gid REGEXP ? OR (allow_cid = '' AND allow_gid = '')))",
$uid, $user_contact_str, $public_contact_str, $group_str,
$user_contact_str, $public_contact_str, $group_str];
} else {
$condition = ["`uid` = ? AND (NOT (`deny_cid` REGEXP ? OR deny_gid REGEXP ?)
AND (allow_cid REGEXP ? OR allow_gid REGEXP ? OR (allow_cid = '' AND allow_gid = '')))",
$uid, $public_contact_str, $group_str, $public_contact_str, $group_str];
}
return $this->select($condition);
}
}

View File

@ -28,7 +28,7 @@ use Friendica\Core\L10n;
use Friendica\Database\Database; use Friendica\Database\Database;
use Friendica\Database\DBA; use Friendica\Database\DBA;
use Friendica\Model; use Friendica\Model;
use Friendica\Util\ACLFormatter; use Friendica\Security\PermissionSet\Depository\PermissionSet;
use Friendica\Util\DateTimeFormat; use Friendica\Util\DateTimeFormat;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
@ -42,17 +42,17 @@ class ProfileField extends BaseRepository
/** @var PermissionSet */ /** @var PermissionSet */
private $permissionSet; private $permissionSet;
/** @var ACLFormatter */ /** @var \Friendica\Security\PermissionSet\Factory\PermissionSet */
private $aclFormatter; private $permissionSetFactory;
/** @var L10n */ /** @var L10n */
private $l10n; private $l10n;
public function __construct(Database $dba, LoggerInterface $logger, PermissionSet $permissionSet, ACLFormatter $aclFormatter, L10n $l10n) public function __construct(Database $dba, LoggerInterface $logger, PermissionSet $permissionSet, \Friendica\Security\PermissionSet\Factory\PermissionSet $permissionSetFactory, L10n $l10n)
{ {
parent::__construct($dba, $logger); parent::__construct($dba, $logger);
$this->permissionSet = $permissionSet; $this->permissionSet = $permissionSet;
$this->aclFormatter = $aclFormatter; $this->permissionSetFactory = $permissionSetFactory;
$this->l10n = $l10n; $this->l10n = $l10n;
} }
@ -176,13 +176,13 @@ class ProfileField extends BaseRepository
// Creation of the new field // Creation of the new field
if (!empty($profileFieldInputs['new']['label'])) { if (!empty($profileFieldInputs['new']['label'])) {
$psid = $this->permissionSet->getIdFromACL( $psid = $this->permissionSet->selectOrCreate($this->permissionSetFactory->createFromString(
$uid, $uid,
$this->aclFormatter->toString($profileFieldInputs['new']['contact_allow'] ?? ''), $profileFieldInputs['new']['contact_allow'] ?? '',
$this->aclFormatter->toString($profileFieldInputs['new']['group_allow'] ?? ''), $profileFieldInputs['new']['group_allow'] ?? '',
$this->aclFormatter->toString($profileFieldInputs['new']['contact_deny'] ?? ''), $profileFieldInputs['new']['contact_deny'] ?? '',
$this->aclFormatter->toString($profileFieldInputs['new']['group_deny'] ?? '') $profileFieldInputs['new']['group_deny'] ?? ''
); ))->id;
$newProfileField = $this->insert([ $newProfileField = $this->insert([
'uid' => $uid, 'uid' => $uid,
@ -220,13 +220,13 @@ class ProfileField extends BaseRepository
// Update existing profile fields from form values // Update existing profile fields from form values
$profileFields = $profileFields->map(function (Model\ProfileField $profileField) use ($uid, &$profileFieldInputs, &$profileFieldOrder) { $profileFields = $profileFields->map(function (Model\ProfileField $profileField) use ($uid, &$profileFieldInputs, &$profileFieldOrder) {
if (isset($profileFieldInputs[$profileField->id]) && isset($profileFieldOrder[$profileField->id])) { if (isset($profileFieldInputs[$profileField->id]) && isset($profileFieldOrder[$profileField->id])) {
$psid = $this->permissionSet->getIdFromACL( $psid = $this->permissionSet->selectOrCreate($this->permissionSetFactory->createFromString(
$uid, $uid,
$this->aclFormatter->toString($profileFieldInputs[$profileField->id]['contact_allow'] ?? ''), $profileFieldInputs[$profileField->id]['contact_allow'] ?? '',
$this->aclFormatter->toString($profileFieldInputs[$profileField->id]['group_allow'] ?? ''), $profileFieldInputs[$profileField->id]['group_allow'] ?? '',
$this->aclFormatter->toString($profileFieldInputs[$profileField->id]['contact_deny'] ?? ''), $profileFieldInputs[$profileField->id]['contact_deny'] ?? '',
$this->aclFormatter->toString($profileFieldInputs[$profileField->id]['group_deny'] ?? '') $profileFieldInputs[$profileField->id]['group_deny'] ?? ''
); ))->id;
$profileField->psid = $psid; $profileField->psid = $psid;
$profileField->label = $profileFieldInputs[$profileField->id]['label']; $profileField->label = $profileFieldInputs[$profileField->id]['label'];
@ -257,17 +257,22 @@ class ProfileField extends BaseRepository
return; return;
} }
$contacts = [];
if (!$profile['is-default']) { if (!$profile['is-default']) {
$contacts = Model\Contact::selectToArray(['id'], ['uid' => $profile['uid'], 'profile-id' => $profile['id']]); $contacts = Model\Contact::selectToArray(['id'], ['uid' => $profile['uid'], 'profile-id' => $profile['id']]);
if (!count($contacts)) { if (!count($contacts)) {
// No contact visibility selected defaults to user-only permission // No contact visibility selected defaults to user-only permission
$contacts = Model\Contact::selectToArray(['id'], ['uid' => $profile['uid'], 'self' => true]); $contacts = Model\Contact::selectToArray(['id'], ['uid' => $profile['uid'], 'self' => true]);
} }
$allow_cid = $this->aclFormatter->toString(array_column($contacts, 'id'));
} }
$psid = $this->permissionSet->getIdFromACL($profile['uid'], $allow_cid ?? ''); $psid = $this->permissionSet->selectOrCreate(
new \Friendica\Security\PermissionSet\Entity\PermissionSet(
$profile['uid'],
array_column($contacts, 'id') ?? []
)
)->id;
$order = 1; $order = 1;

View File

@ -0,0 +1,9 @@
<?php
namespace Friendica\Security\PermissionSet\Collection;
use Friendica\BaseCollection;
class PermissionSets extends BaseCollection
{
}

View File

@ -0,0 +1,214 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Security\PermissionSet\Depository;
use Exception;
use Friendica\BaseDepository;
use Friendica\Database\Database;
use Friendica\Model\Contact;
use Friendica\Model\Group;
use Friendica\Network\HTTPException\NotFoundException;
use Friendica\Security\PermissionSet\Factory;
use Friendica\Security\PermissionSet\Collection;
use Friendica\Security\PermissionSet\Entity;
use Friendica\Util\ACLFormatter;
use Psr\Log\LoggerInterface;
class PermissionSet extends BaseDepository
{
/** @var int Virtual permission set id for public permission */
const PUBLIC = 0;
/** @var Factory\PermissionSet */
protected $factory;
protected static $table_name = 'permissionset';
/** @var ACLFormatter */
private $aclFormatter;
public function __construct(Database $database, LoggerInterface $logger, Factory\PermissionSet $factory, ACLFormatter $aclFormatter)
{
parent::__construct($database, $logger, $factory);
$this->aclFormatter = $aclFormatter;
}
/**
* @param array $condition
* @param array $params
*
* @return Entity\PermissionSet
* @throws NotFoundException
*/
private function selectOne(array $condition, array $params = []): Entity\PermissionSet
{
return parent::_selectOne($condition, $params);
}
private function select(array $condition, array $params = []): Collection\PermissionSets
{
return new Collection\PermissionSets(parent::_select($condition, $params)->getArrayCopy());
}
/**
* Converts a given PermissionSet into a DB compatible row array
*
* @param Entity\PermissionSet $permissionSet
*
* @return array
*/
protected function convertToTableRow(Entity\PermissionSet $permissionSet): array
{
return [
'uid' => $permissionSet->uid,
'allow_cid' => $this->aclFormatter->toString($permissionSet->allow_cid),
'allow_gid' => $this->aclFormatter->toString($permissionSet->allow_gid),
'deny_cid' => $this->aclFormatter->toString($permissionSet->deny_cid),
'deny_gid' => $this->aclFormatter->toString($permissionSet->deny_gid),
];
}
/**
* @param int $id
*
* @return Entity\PermissionSet
* @throws NotFoundException
*/
public function selectOneById(int $id): Entity\PermissionSet
{
return $this->selectOne(['id' => $id]);
}
/**
* Returns a permission set collection for a given contact
*
* @param int $cid Contact id of the visitor
* @param int $uid User id whom the items belong, used for ownership check.
*
* @return Collection\PermissionSets
*/
public function selectByContactId(int $cid, int $uid): Collection\PermissionSets
{
$cdata = Contact::getPublicAndUserContactID($cid, $uid);
if (!empty($cdata)) {
$public_contact_str = $this->aclFormatter->toString($cdata['public']);
$user_contact_str = $this->aclFormatter->toString($cdata['user']);
$cid = $cdata['user'];
} else {
$public_contact_str = $this->aclFormatter->toString($cid);
$user_contact_str = '';
}
$groups = [];
if (!empty($user_contact_str) && $this->db->exists('contact', [
'id' => $cid,
'uid' => $uid,
'blocked' => false
])) {
$groups = Group::getIdsByContactId($cid);
}
$group_str = '<<>>'; // should be impossible to match
foreach ($groups as $group_id) {
$group_str .= '|<' . preg_quote($group_id) . '>';
}
if (!empty($user_contact_str)) {
$condition = ["`uid` = ? AND (NOT (`deny_cid` REGEXP ? OR `deny_cid` REGEXP ? OR deny_gid REGEXP ?)
AND (allow_cid REGEXP ? OR allow_cid REGEXP ? OR allow_gid REGEXP ? OR (allow_cid = '' AND allow_gid = '')))",
$uid, $user_contact_str, $public_contact_str, $group_str,
$user_contact_str, $public_contact_str, $group_str];
} else {
$condition = ["`uid` = ? AND (NOT (`deny_cid` REGEXP ? OR deny_gid REGEXP ?)
AND (allow_cid REGEXP ? OR allow_gid REGEXP ? OR (allow_cid = '' AND allow_gid = '')))",
$uid, $public_contact_str, $group_str, $public_contact_str, $group_str];
}
return $this->select($condition);
}
/**
* Fetch the default PermissionSet for a given user, create it if it doesn't exist
*
* @param int $uid
*
* @return Entity\PermissionSet
* @throws Exception
*/
public function selectDefaultForUser(int $uid): Entity\PermissionSet
{
$self_contact = Contact::selectFirst(['id'], ['uid' => $uid, 'self' => true]);
return $this->selectOrCreate($this->factory->createFromString(
$uid,
$this->aclFormatter->toString($self_contact['id'])
));
}
/**
* Fetch the empty PermissionSet for a given user, create it if it doesn't exist
*
* @param int $uid
*
* @return Entity\PermissionSet
*/
public function selectEmptyForUser(int $uid): Entity\PermissionSet
{
return $this->selectOrCreate($this->factory->createFromString($uid));
}
/**
* Selects or creates a PermissionSet based on it's fields
*
* @param Entity\PermissionSet $permissionSet
*
* @return Entity\PermissionSet
*/
public function selectOrCreate(Entity\PermissionSet $permissionSet): Entity\PermissionSet
{
if ($permissionSet->id) {
return $permissionSet;
}
try {
return $this->selectOne($this->convertToTableRow($permissionSet));
} catch (NotFoundException $exception) {
return $this->save($permissionSet);
}
}
public function save(Entity\PermissionSet $permissionSet): Entity\PermissionSet
{
$fields = $this->convertToTableRow($permissionSet);
if ($permissionSet->id) {
$this->db->update(self::$table_name, $fields, ['id' => $permissionSet->id]);
} else {
$this->db->insert(self::$table_name, $fields);
$permissionSet = $this->selectOneById($this->db->lastInsertId());
}
return $permissionSet;
}
}

View File

@ -0,0 +1,66 @@
<?php
namespace Friendica\Security\PermissionSet\Entity;
use Friendica\BaseEntity;
/**
* @property-read int|null $id
* @property-read int $uid
* @property-read string[] $allow_cid
* @property-read string[] $allow_gid
* @property-read string[] $deny_cid
* @property-read string[] $deny_gid
*/
class PermissionSet extends BaseEntity
{
/** @var int|null */
protected $id;
/** @var int */
protected $uid;
/** @var string[] */
protected $allow_cid;
/** @var string[] */
protected $allow_gid;
/** @var string[] */
protected $deny_cid;
/** @var string[] */
protected $deny_gid;
/**
* @param int|null $id
* @param int $uid
* @param string[] $allow_cid
* @param string[] $allow_gid
* @param string[] $deny_cid
* @param string[] $deny_gid
*
* @see \Friendica\Security\PermissionSet\Factory\PermissionSet
*/
public function __construct(int $uid, array $allow_cid = [], array $allow_gid = [], array $deny_cid = [], array $deny_gid = [], int $id = null)
{
$this->id = $id;
$this->uid = $uid;
$this->allow_cid = $allow_cid;
$this->allow_gid = $allow_gid;
$this->deny_cid = $deny_cid;
$this->deny_gid = $deny_gid;
}
/**
* Creates a new Entity with a new allowed_cid list (wipes the id because it isn't the same entity anymore)
*
* @param array $allow_cid
*
* @return $this
*/
public function withAllowedContacts(array $allow_cid): PermissionSet
{
$clone = clone $this;
$clone->allow_cid = $allow_cid;
$clone->id = null;
return $clone;
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace Friendica\Security\PermissionSet\Factory;
use Friendica\BaseFactory;
use Friendica\Capabilities\ICanCreateFromTableRow;
use Friendica\Security\PermissionSet\Entity;
use Friendica\Util\ACLFormatter;
use Psr\Log\LoggerInterface;
class PermissionSet extends BaseFactory implements ICanCreateFromTableRow
{
/** @var ACLFormatter */
protected $formatter;
public function __construct(LoggerInterface $logger, ACLFormatter $formatter)
{
parent::__construct($logger);
$this->formatter = $formatter;
}
/**
* @inheritDoc
*/
public function createFromTableRow(array $row): Entity\PermissionSet
{
return new Entity\PermissionSet(
$row['uid'],
$this->formatter->expand($row['allow_cid'] ?? ''),
$this->formatter->expand($row['allow_gid'] ?? ''),
$this->formatter->expand($row['deny_cid'] ?? ''),
$this->formatter->expand($row['deny_gid'] ?? ''),
$row['id'] ?? null
);
}
/**
* Creates a new PermissionSet based on it's fields
*
* @param int $uid
* @param string $allow_cid
* @param string $allow_gid
* @param string $deny_cid
* @param string $deny_gid
*
* @return Entity\PermissionSet
*/
public function createFromString(
int $uid,
string $allow_cid = '',
string $allow_gid = '',
string $deny_cid = '',
string $deny_gid = ''): Entity\PermissionSet
{
return $this->createFromTableRow([
'uid' => $uid,
'allow_cid' => $allow_cid,
'allow_gid' => $allow_gid,
'deny_cid' => $deny_cid,
'deny_gid' => $deny_gid,
]);
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace Friendica\Test\src\Security\PermissionSet\Entity;
use Friendica\Security\PermissionSet\Entity\PermissionSet;
use Friendica\Test\MockedTest;
class PermissionSetTest extends MockedTest
{
public function dateAllowedContacts()
{
return [
'default' => [
'id' => 10,
'allow_cid' => ['1', '2'],
'allow_gid' => ['3', '4'],
'deny_cid' => ['5', '6', '7'],
'deny_gid' => ['8'],
'update_cid' => ['10'],
],
];
}
/**
* Test if the call "withAllowedContacts()" creates a clone
*
* @dataProvider dateAllowedContacts
*/
public function testWithAllowedContacts(int $id, array $allow_cid, array $allow_gid, array $deny_cid, array $deny_gid, array $update_cid)
{
$permissionSetOrig = new PermissionSet(
$id,
$allow_cid,
$allow_gid,
$deny_cid,
$deny_gid
);
$permissionSetNew = $permissionSetOrig->withAllowedContacts($update_cid);
self::assertNotSame($permissionSetOrig, $permissionSetNew);
self::assertEquals($update_cid, $permissionSetNew->allow_cid);
self::assertEquals($allow_cid, $permissionSetOrig->allow_cid);
}
}

View File

@ -0,0 +1,134 @@
<?php
namespace Friendica\Test\src\Security\PermissionSet\Factory;
use Friendica\Security\PermissionSet\Factory\PermissionSet;
use Friendica\Test\MockedTest;
use Friendica\Util\ACLFormatter;
use Psr\Log\NullLogger;
class PermissionSetTest extends MockedTest
{
/** @var PermissionSet */
protected $permissionSet;
protected function setUp(): void
{
parent::setUp();
$this->permissionSet = new PermissionSet(new NullLogger(), new ACLFormatter());
}
public function dataInput()
{
return [
'new' => [
'input' => [
'uid' => 12,
'allow_cid' => '<1>,<2>',
'allow_gid' => '<3>,<4>',
'deny_cid' => '<6>',
'deny_gid' => '<8>',
],
'assertion' => [
'id' => null,
'uid' => 12,
'allow_cid' => ['1', '2'],
'allow_gid' => ['3', '4'],
'deny_cid' => ['6'],
'deny_gid' => ['8'],
],
],
'full' => [
'input' => [
'id' => 3,
'uid' => 12,
'allow_cid' => '<1>,<2>',
'allow_gid' => '<3>,<4>',
'deny_cid' => '<6>',
'deny_gid' => '<8>',
],
'assertion' => [
'id' => 3,
'uid' => 12,
'allow_cid' => ['1', '2'],
'allow_gid' => ['3', '4'],
'deny_cid' => ['6'],
'deny_gid' => ['8'],
],
],
'mini' => [
'input' => [
'id' => null,
'uid' => 12,
],
'assertion' => [
'id' => null,
'uid' => 12,
'allow_cid' => [],
'allow_gid' => [],
'deny_cid' => [],
'deny_gid' => [],
],
],
'wrong' => [
'input' => [
'id' => 3,
'uid' => 12,
'allow_cid' => '<1,<2>',
],
'assertion' => [
'id' => 3,
'uid' => 12,
'allow_cid' => ['2'],
'allow_gid' => [],
'deny_cid' => [],
'deny_gid' => [],
],
]
];
}
protected function assertPermissionSet(\Friendica\Security\PermissionSet\Entity\PermissionSet $permissionSet, array $assertion)
{
self::assertEquals($assertion['id'] ?? null, $permissionSet->id);
self::assertNotNull($permissionSet->uid);
self::assertEquals($assertion['uid'], $permissionSet->uid);
self::assertEquals($assertion['allow_cid'], $permissionSet->allow_cid);
self::assertEquals($assertion['allow_gid'], $permissionSet->allow_gid);
self::assertEquals($assertion['deny_cid'], $permissionSet->deny_cid);
self::assertEquals($assertion['deny_gid'], $permissionSet->deny_gid);
}
/**
* Test the createFromTableRow method
*
* @dataProvider dataInput
*/
public function testCreateFromTableRow(array $input, array $assertion)
{
$permissionSet = $this->permissionSet->createFromTableRow($input);
$this->assertPermissionSet($permissionSet, $assertion);
}
/**
* Test the createFromString method
*
* @dataProvider dataInput
*/
public function testCreateFromString(array $input, array $assertion)
{
$permissionSet = $this->permissionSet->createFromString(
$input['uid'],
$input['allow_cid'] ?? '',
$input['allow_gid'] ?? '',
$input['deny_cid'] ?? '',
$input['deny_gid'] ?? ''
);
unset($assertion['id']);
$this->assertPermissionSet($permissionSet, $assertion);
}
}