Skip to content

Commit

Permalink
Merge pull request #945 from nextcloud/feat/898
Browse files Browse the repository at this point in the history
feat: Transfer context ownership
  • Loading branch information
blizzz authored Apr 3, 2024
2 parents 50cb388 + 72afabb commit b0a793e
Show file tree
Hide file tree
Showing 11 changed files with 310 additions and 35 deletions.
7 changes: 7 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ class Application extends App implements IBootstrap {
public const NAV_ENTRY_MODE_RECIPIENTS = 1;
public const NAV_ENTRY_MODE_ALL = 2;

public const PERMISSION_READ = 1;
public const PERMISSION_CREATE = 2;
public const PERMISSION_UPDATE = 4;
public const PERMISSION_DELETE = 8;
public const PERMISSION_MANAGE = 16;
public const PERMISSION_ALL = 31;

public function __construct() {
parent::__construct(self::APP_ID);
}
Expand Down
34 changes: 34 additions & 0 deletions lib/Db/ContextMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,40 @@ public function findById(int $contextId, ?string $userId = null): Context {
return $this->formatResultRows($r, $userId);
}

/**
* @return Context[]
* @throws Exception
*/
public function findAllContainingNode(int $nodeType, int $nodeId, string $userId): array {
$qb = $this->getFindContextBaseQuery($userId);

$qb->andWhere($qb->expr()->eq('r.node_id', $qb->createNamedParameter($nodeId)))
->andWhere($qb->expr()->eq('r.node_type', $qb->createNamedParameter($nodeType)));

$result = $qb->executeQuery();
$r = $result->fetchAll();

$contextIds = [];
foreach ($r as $row) {
$contextIds[$row['id']] = 1;
}
$contextIds = array_keys($contextIds);
unset($row);

$resultEntities = [];
foreach ($contextIds as $contextId) {
$workArray = [];
foreach ($r as $row) {
if ($row['id'] === $contextId) {
$workArray[] = $row;
}
}
$resultEntities[] = $this->formatResultRows($workArray, $userId);
}

return $resultEntities;
}

protected function applyOwnedOrSharedQuery(IQueryBuilder $qb, string $userId): void {
$sharedToConditions = $qb->expr()->orX();

Expand Down
94 changes: 77 additions & 17 deletions lib/Service/PermissionsService.php
Original file line number Diff line number Diff line change
Expand Up @@ -153,20 +153,7 @@ public function canManageContextById(int $contextId, ?string $userId = null): bo
}

public function canAccessView(View $view, ?string $userId = null): bool {
if($this->basisCheck($view, 'view', $userId)) {
return true;
}

if ($userId) {
try {
$this->getSharedPermissionsIfSharedWithMe($view->getId(), 'view', $userId);
return true;
} catch (NotFoundError $e) {
$this->logger->error($e->getMessage(), ['exception' => $e]);
}
}

return false;
return $this->canAccessNodeById(Application::NODE_TYPE_VIEW, $view->getId(), $userId);
}

/**
Expand Down Expand Up @@ -458,6 +445,64 @@ public function getSharedPermissionsIfSharedWithMe(int $elementId, string $eleme

// private methods ==========================================================================

/**
* @throws NotFoundError
*/
public function getPermissionIfAvailableThroughContext(int $nodeId, string $nodeType, string $userId): int {
$permissions = 0;
$found = false;
$iNodeType = match ($nodeType) {
'table' => Application::NODE_TYPE_TABLE,
'view' => Application::NODE_TYPE_VIEW,
};
$contexts = $this->contextMapper->findAllContainingNode($iNodeType, $nodeId, $userId);
foreach ($contexts as $context) {
$found = true;
if ($context->getOwnerType() === Application::OWNER_TYPE_USER
&& $context->getOwnerId() === $userId) {
// Making someone owner of a context, makes this person also having manage permissions on the node.
// This is sort of an intended "privilege escalation".
return Application::PERMISSION_ALL;
}
foreach ($context->getNodes() as $nodeRelation) {
$permissions |= $nodeRelation['permissions'];
}
}
if (!$found) {
throw new NotFoundError('Node not found in any context');
}
return $permissions;
}

/**
* @throws NotFoundError
*/
public function getPermissionArrayForNodeFromContexts(int $nodeId, string $nodeType, string $userId) {
$permissions = $this->getPermissionIfAvailableThroughContext($nodeId, $nodeType, $userId);
return [
'read' => (bool)($permissions & Application::PERMISSION_READ),
'create' => (bool)($permissions & Application::PERMISSION_CREATE),
'update' => (bool)($permissions & Application::PERMISSION_UPDATE),
'delete' => (bool)($permissions & Application::PERMISSION_DELETE),
'manage' => (bool)($permissions & Application::PERMISSION_MANAGE),
];
}

private function hasPermission(int $existingPermissions, string $permissionName): bool {
$constantName = 'PERMISSION_' . strtoupper($permissionName);
try {
$permissionBit = constant(Application::class . "::$constantName");
} catch (\Throwable $t) {
$this->logger->error('Unexpected permission string {permission}', [
'app' => Application::APP_ID,
'permission' => $permissionName,
'exception' => $t,
]);
return false;
}
return (bool)($existingPermissions & $permissionBit);
}

/**
* @param mixed $element
* @param 'table'|'view' $nodeType
Expand All @@ -470,13 +515,22 @@ private function checkPermission($element, string $nodeType, string $permission,
return true;
}

if ($userId) {
if (!$userId) {
return false;
}

try {
return $this->getSharedPermissionsIfSharedWithMe($element->getId(), $nodeType, $userId)[$permission];
} catch (NotFoundError $e) {
try {
return $this->getSharedPermissionsIfSharedWithMe($element->getId(), $nodeType, $userId)[$permission];
if ($this->hasPermission($this->getPermissionIfAvailableThroughContext($element->getId(), $nodeType, $userId), $permission)) {
return true;
}
} catch (NotFoundError $e) {
$this->logger->error($e->getMessage(), ['exception' => $e]);
}
$this->logger->error($e->getMessage(), ['exception' => $e]);
}

return false;
}

Expand All @@ -495,6 +549,12 @@ private function checkPermissionById(int $elementId, string $nodeType, string $p
try {
return $this->getSharedPermissionsIfSharedWithMe($elementId, $nodeType, $userId)[$permission];
} catch (NotFoundError $e) {
try {
if ($this->hasPermission($this->getPermissionIfAvailableThroughContext($elementId, $nodeType, $userId), $permission)) {
return true;
}
} catch (NotFoundError $e) {
}
$this->logger->error($e->getMessage(), ['exception' => $e]);
}
}
Expand Down
6 changes: 6 additions & 0 deletions lib/Service/TableService.php
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,12 @@ private function enhanceTable(Table $table, string $userId): void {
$table->setIsShared(true);
$table->setOnSharePermissions($permissions);
} catch (NotFoundError $e) {
try {
$table->setOnSharePermissions($this->permissionsService->getPermissionArrayForNodeFromContexts($table->getId(), 'table', $userId));
$table->setIsShared(true);
} catch (NotFoundError $e) {
}

}
}
if (!$table->getIsShared() || $table->getOnSharePermissions()['manage']) {
Expand Down
23 changes: 12 additions & 11 deletions lib/Service/ViewService.php
Original file line number Diff line number Diff line change
Expand Up @@ -322,24 +322,25 @@ private function enhanceView(View $view, string $userId): void {
if ($userId !== '') {
if ($userId !== $view->getOwnership()) {
try {
$permissions = $this->shareService->getSharedPermissionsIfSharedWithMe($view->getId(), 'view', $userId);
try {
$permissions = $this->shareService->getSharedPermissionsIfSharedWithMe($view->getId(), 'view', $userId);
} catch (NotFoundError) {
$permissions = $this->permissionsService->getPermissionArrayForNodeFromContexts($view->getId(), 'view', $userId);
}
$view->setIsShared(true);
$canManageTable = false;
try {
$manageTableShare = $this->shareService->getSharedPermissionsIfSharedWithMe($view->getTableId(), 'table', $userId);
$canManageTable = $manageTableShare['manage'] ?? false;
try {
$manageTableShare = $this->shareService->getSharedPermissionsIfSharedWithMe($view->getTableId(), 'table', $userId);
} catch (NotFoundError) {
$manageTableShare = $this->permissionsService->getPermissionArrayForNodeFromContexts($view->getTableId(), 'table', $userId);
}
$permissions['manageTable'] = $manageTableShare['manage'] ?? false;
} catch (NotFoundError $e) {
} catch (\Exception $e) {
throw new InternalError($e->getMessage());
}
$view->setOnSharePermissions([
'read' => $permissions['read'] ?? false,
'create' => $permissions['create'] ?? false,
'update' => $permissions['update'] ?? false,
'delete' => $permissions['delete'] ?? false,
'manage' => $permissions['manage'] ?? false,
'manageTable' => $canManageTable
]);
$view->setOnSharePermissions($permissions);
} catch (NotFoundError $e) {
} catch (\Exception $e) {
$this->logger->warning('Exception occurred while setting shared permissions: '.$e->getMessage().' No permissions granted.');
Expand Down
6 changes: 6 additions & 0 deletions src/modules/modals/Modals.vue
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
<EditTable :table-id="editTable" :show-modal="editTable !== null" @close="editTable = null" />
<TransferTable :table="tableToTransfer" :show-modal="tableToTransfer !== null" @close="tableToTransfer = null" />
<EditContext :context-id="editContext" :show-modal="editContext !== null" @close="editContext = null" />
<TransferContext :context="contextToTransfer" :show-modal="contextToTransfer !== null" @close="contextToTransfer = null" />
</div>
</template>

Expand All @@ -60,6 +61,7 @@ import EditTable from './EditTable.vue'
import EditContext from './EditContext.vue'
import TransferTable from './TransferTable.vue'
import CreateContext from './CreateContext.vue'
import TransferContext from './TransferContext.vue'
export default {
components: {
Expand All @@ -78,6 +80,7 @@ export default {
TransferTable,
CreateContext,
EditContext,
TransferContext,
},
data() {
Expand All @@ -98,6 +101,7 @@ export default {
editTable: null,
editContext: null,
tableToTransfer: null,
contextToTransfer: null,
}
},
Expand Down Expand Up @@ -138,6 +142,7 @@ export default {
// context
subscribe('tables:context:create', () => { this.showModalCreateContext = true })
subscribe('tables:context:edit', contextId => { this.editContext = contextId })
subscribe('tables:context:transfer', context => { this.contextToTransfer = context })
},
unmounted() {
Expand Down Expand Up @@ -165,6 +170,7 @@ export default {
unsubscribe('tables:table:transfer', table => { this.tableToTransfer = table })
unsubscribe('tables:context:create', () => { this.showModalCreateContext = true })
unsubscribe('tables:context:edit', contextId => { this.editContext = contextId })
unsubscribe('tables:context:transfer', context => { this.contextToTransfer = context })
},
}
</script>
89 changes: 89 additions & 0 deletions src/modules/modals/TransferContext.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<template>
<NcModal v-if="showModal"
size="normal"
@close="actionCancel">
<div class="modal__content" data-cy="transferContextModal">
<div class="row">
<div class="col-4">
<h2>{{ t('tables', 'Transfer application') }}</h2>
</div>
</div>
<div class="row">
<h3>{{ t('tables', 'Transfer this application to another user') }}</h3>
<NcUserAndGroupPicker :select-users="true" :select-groups="false" :new-owner-user-id.sync="newOwnerId" />
</div>
<div class="row">
<div class="fix-col-4 space-T end">
<NcButton type="warning" :disabled="newOwnerId === ''" data-cy="transferTableButton" @click="transferContext">
{{ t('tables', 'Transfer') }}
</NcButton>
</div>
</div>
</div>
</NcModal>
</template>

<script>
import { NcModal, NcButton } from '@nextcloud/vue'
import { showSuccess } from '@nextcloud/dialogs'
import '@nextcloud/dialogs/dist/index.css'
import permissionsMixin from '../../shared/components/ncTable/mixins/permissionsMixin.js'
import NcUserAndGroupPicker from '../../shared/components/ncUserAndGroupPicker/NcUserAndGroupPicker.vue'
import { mapGetters, mapState } from 'vuex'
import { getCurrentUser } from '@nextcloud/auth'
export default {
name: 'TransferContext',
components: {
NcModal,
NcButton,
NcUserAndGroupPicker,
},
mixins: [permissionsMixin],
props: {
showModal: {
type: Boolean,
default: false,
},
context: {
type: Object,
default: null,
},
},
data() {
return {
loading: false,
newOwnerId: '',
}
},
computed: {
...mapGetters(['getContext']),
...mapState(['activeContextId']),
localContext() {
return this.getContext(this.context.id)
},
userId() {
return getCurrentUser().uid
},
},
methods: {
actionCancel() {
this.$emit('close')
},
async transferContext() {
const transferId = this.context.id
const res = await this.$store.dispatch('transferContext', { id: this.context.id, data: { newOwnerId: this.newOwnerId } })
if (res) {
showSuccess(t('tables', 'Context "{name}" transfered to {user}', { name: this.context?.name, user: this.newOwnerId }))
if (transferId === this.activeContextId) {
await this.$router.push('/').catch(err => err)
}
this.actionCancel()
}
},
},
}
</script>
Loading

0 comments on commit b0a793e

Please sign in to comment.