UBERF-5812: Fix allow to delete based on all my accounts (#4823)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2024-03-01 23:39:46 +07:00 committed by GitHub
parent 5cd8dca583
commit ae6059c040
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 124 additions and 62 deletions

View File

@ -326,7 +326,7 @@ function defineApplication (
spaceClass: tracker.class.Project,
componentProps: {
_class: tracker.class.Project,
label: tracker.string.AllIssues,
label: tracker.string.AllProjects,
icon: tracker.icon.Issues
}
}

View File

@ -187,12 +187,22 @@ export async function setClient (_client: MeasureClient): Promise<void> {
* @public
*/
export async function refreshClient (): Promise<void> {
await liveQuery?.refreshConnect()
for (const q of globalQueries) {
q.refreshClient()
if (!(liveQuery?.isClosed() ?? true)) {
await liveQuery?.refreshConnect()
for (const q of globalQueries) {
q.refreshClient()
}
}
}
/**
* @public
*/
export async function purgeClient (): Promise<void> {
await liveQuery?.close()
await pipeline?.close()
}
/**
* @public
*/
@ -529,13 +539,9 @@ export function isCollectionAttr (hierarchy: Hierarchy, key: KeyedAttribute): bo
* @public
*/
export function decodeTokenPayload (token: string): any {
const parts = token.split('.')
const payload = parts[1]
const decodedPayload = atob(payload)
const parsedPayload = JSON.parse(decodedPayload)
return parsedPayload
return JSON.parse(atob(token.split('.')[1]))
}
export function isAdminUser (): boolean {
return decodeTokenPayload(getMetadata(plugin.metadata.Token) ?? '').admin === 'true'
}

View File

@ -91,6 +91,7 @@ export class LiveQuery implements WithTx, Client {
private readonly queries: Map<Ref<Class<Doc>>, Query[]> = new Map<Ref<Class<Doc>>, Query[]>()
private readonly queue: Query[] = []
private queryCounter: number = 0
private closed: boolean = false
// A map of _class to documents.
private readonly documentRefs = new Map<string, Map<Ref<Doc>, DocumentRef>>()
@ -104,7 +105,12 @@ export class LiveQuery implements WithTx, Client {
await this.refreshConnect()
}
public isClosed (): boolean {
return this.closed
}
async close (): Promise<void> {
this.closed = true
await this.client.close()
}

View File

@ -13,16 +13,16 @@
// limitations under the License.
-->
<script lang="ts">
import { Card } from '@hcengineering/presentation'
import { AccountRole, Doc, getCurrentAccount, Ref } from '@hcengineering/core'
import { PersonAccount } from '@hcengineering/contact'
import { AccountRole, Doc, Ref, getCurrentAccount } from '@hcengineering/core'
import { Card, isAdminUser } from '@hcengineering/presentation'
import ui, { Button, Label } from '@hcengineering/ui'
import { ObjectPresenter } from '@hcengineering/view-resources'
import view from '@hcengineering/view-resources/src/plugin'
import { createEventDispatcher } from 'svelte'
import { PersonAccount } from '@hcengineering/contact'
import { personAccountByIdStore } from '../utils'
import ui, { Button, Label } from '@hcengineering/ui'
import PersonAccountRefPresenter from './PersonAccountRefPresenter.svelte'
import PersonAccountPresenter from './PersonAccountPresenter.svelte'
import { ObjectPresenter } from '@hcengineering/view-resources'
import PersonAccountRefPresenter from './PersonAccountRefPresenter.svelte'
export let object: Doc | Doc[]
export let deleteAction: () => void | Promise<void>
@ -37,7 +37,8 @@
$: canDelete =
(skipCheck ||
(creators.length === 1 && creators.includes(getCurrentAccount()._id as Ref<PersonAccount>)) ||
getCurrentAccount().role === AccountRole.Owner) &&
getCurrentAccount().role === AccountRole.Owner ||
isAdminUser()) &&
canDeleteExtra
$: label = canDelete ? view.string.DeleteObject : view.string.DeletePopupNoPermissionTitle
</script>

View File

@ -16,20 +16,21 @@
import contact, { Employee } from '@hcengineering/contact'
import { AccountRole, getCurrentAccount, Ref } from '@hcengineering/core'
import { tzDateCompare, type Department, type Request, type RequestType, type Staff } from '@hcengineering/hr'
import { isAdminUser } from '@hcengineering/presentation'
import {
areDatesEqual,
day as getDay,
daysInMonth,
deviceOptionsStore as deviceInfo,
eventToHTMLElement,
day as getDay,
getWeekDayName,
isWeekend,
Label,
LabelAndProps,
resizeObserver,
Scroller,
showPopup,
tooltip,
deviceOptionsStore as deviceInfo,
resizeObserver
tooltip
} from '@hcengineering/ui'
import hr from '../../plugin'
import { getHolidayDatesForEmployee, getRequests, isHoliday } from '../../utils'
@ -183,7 +184,7 @@
}
function isEditable (employee: Staff): boolean {
return editableList.includes(employee._id) && (isFutureDate() || me.role === AccountRole.Owner)
return editableList.includes(employee._id) && (isFutureDate() || me.role === AccountRole.Owner || isAdminUser())
}
function getTooltip (requests: Request[], day: Date, staff: Staff): LabelAndProps | undefined {
@ -306,7 +307,7 @@
class="timeline-cell timeline-day-header flex-col-center justify-center"
style={getCellStyle()}
on:click={() => {
if (isFutureDate() || me.role === AccountRole.Owner) {
if (isFutureDate() || me.role === AccountRole.Owner || isAdminUser()) {
setPublicHoliday(day)
}
}}

View File

@ -15,7 +15,7 @@
<script lang="ts">
import { PersonAccount } from '@hcengineering/contact'
import { AccountRole, getCurrentAccount, roleOrder } from '@hcengineering/core'
import presentation, { createQuery, decodeTokenPayload } from '@hcengineering/presentation'
import { createQuery, isAdminUser } from '@hcengineering/presentation'
import setting, { SettingsCategory } from '@hcengineering/setting'
import {
Component,
@ -27,7 +27,6 @@
} from '@hcengineering/ui'
import { onDestroy } from 'svelte'
import { clearSettingsStore } from '../store'
import { getMetadata } from '@hcengineering/platform'
export let kind: 'navigation' | 'content' | undefined
export let categoryName: string
@ -39,7 +38,7 @@
let categories: SettingsCategory[] = []
const account = getCurrentAccount() as PersonAccount
const admin = decodeTokenPayload(getMetadata(presentation.metadata.Token) ?? '').admin === 'true'
const admin = isAdminUser()
const settingsQuery = createQuery()
settingsQuery.query(
@ -48,7 +47,7 @@
(res) => {
categories = roleOrder[account.role] > roleOrder[AccountRole.User] ? res : res.filter((p) => !p.secured)
if (!admin) {
categories = categories.filter((p) => !p.adminOnly)
categories = categories.filter((p) => !(p.adminOnly ?? false))
}
category = findCategory(categoryId)
},

View File

@ -14,9 +14,17 @@
-->
<script lang="ts">
import { Project } from '@hcengineering/tracker'
import { Icon, IconWithEmoji, getPlatformColorDef, getPlatformColorForTextDef, themeStore } from '@hcengineering/ui'
import {
Icon,
IconWithEmoji,
getPlatformColorDef,
getPlatformColorForTextDef,
themeStore,
Label
} from '@hcengineering/ui'
import tracker from '../../plugin'
import view from '@hcengineering/view'
import presentation from '@hcengineering/presentation'
export let value: Project | undefined
export let inline: boolean = false
@ -41,6 +49,9 @@
</div>
<span class="label no-underline nowrap" class:fs-bold={accent}>
{value.name}
{#if value.archived}
<Label label={presentation.string.Archived} />
{/if}
</span>
</div>
{/if}

View File

@ -17,7 +17,12 @@
import { Analytics } from '@hcengineering/analytics'
import core, {
AccountRole,
type FindOptions,
ClassifierKind,
Hierarchy,
TxProcessor,
getCurrentAccount,
getObjectValue,
type Account,
type AggregateValue,
type AttachedDoc,
type CategoryType,
@ -27,9 +32,7 @@ import core, {
type Doc,
type DocumentQuery,
type DocumentUpdate,
getCurrentAccount,
getObjectValue,
Hierarchy,
type FindOptions,
type Lookup,
type Mixin,
type Obj,
@ -38,38 +41,37 @@ import core, {
type ReverseLookup,
type ReverseLookups,
type Space,
type TxOperations,
type TxCUD,
type TxCollectionCUD,
TxProcessor,
type TxCreateDoc,
type TxUpdateDoc,
type TxMixin,
ClassifierKind,
type TxOperations,
type TxUpdateDoc,
type TypeAny
} from '@hcengineering/core'
import { type Restrictions } from '@hcengineering/guest'
import type { Asset, IntlString } from '@hcengineering/platform'
import { getResource, translate } from '@hcengineering/platform'
import {
type AttributeCategory,
getAttributePresenterClass,
getClient,
hasResource,
isAdminUser,
type AttributeCategory,
type KeyedAttribute
} from '@hcengineering/presentation'
import { type Restrictions } from '@hcengineering/guest'
import {
type AnyComponent,
type AnySvelteComponent,
ErrorPresenter,
getCurrentResolvedLocation,
getPanelURI,
getPlatformColorForText,
type Location,
locationToUrl,
navigate,
resolvedLocationStore,
themeStore
themeStore,
type AnyComponent,
type AnySvelteComponent,
type Location
} from '@hcengineering/ui'
import view, {
type AttributeModel,
@ -80,10 +82,10 @@ import view, {
type ViewletDescriptor
} from '@hcengineering/view'
import contact, { getName, type Contact, type PersonAccount } from '@hcengineering/contact'
import { get, writable } from 'svelte/store'
import plugin from './plugin'
import { noCategory } from './viewOptions'
import contact, { type Contact, getName } from '@hcengineering/contact'
export { getFiltredKeys, isCollectionAttr } from '@hcengineering/presentation'
@ -386,7 +388,8 @@ export async function buildModel (options: BuildModelOptions): Promise<Attribute
export async function deleteObject (client: TxOperations, object: Doc): Promise<void> {
const currentAcc = getCurrentAccount()
if (currentAcc.role !== AccountRole.Owner && object.createdBy !== currentAcc._id) return
const accounts = await getCurrentPersonAccounts()
if (currentAcc.role !== AccountRole.Owner && !accounts.has(object.createdBy)) return
if (client.getHierarchy().isDerived(object._class, core.class.AttachedDoc)) {
const adoc = object as AttachedDoc
await client
@ -401,13 +404,45 @@ export async function deleteObject (client: TxOperations, object: Doc): Promise<
}
}
export async function getCurrentPersonAccounts (): Promise<Set<Ref<Account> | undefined>> {
return new Set(
(
await getClient().findAll(contact.class.PersonAccount, { person: (getCurrentAccount() as PersonAccount).person })
).map((it) => it._id)
)
}
export async function deleteObjects (client: TxOperations, objects: Doc[], skipCheck: boolean = false): Promise<void> {
let realObjects: Doc[] = []
if (!skipCheck) {
const currentAcc = getCurrentAccount()
if (currentAcc.role !== AccountRole.Owner && objects.some((p) => p.createdBy !== currentAcc._id)) return
// We need to find all person current accounts
const allPersonAccounts = await getCurrentPersonAccounts()
const byClass = new Map<Ref<Class<Doc>>, Doc[]>()
for (const d of objects) {
byClass.set(d._class, [...(byClass.get(d._class) ?? []), d])
}
const adminUser = isAdminUser()
for (const [cl, docs] of byClass.entries()) {
const realDocs = await client.findAll(cl, { _id: { $in: docs.map((it: Doc) => it._id) } })
const notAllowed = realDocs.filter((p) => !allPersonAccounts.has(p.createdBy))
if (notAllowed.length > 0) {
console.error('You are not allowed to delete this object', notAllowed)
}
if (currentAcc.role === AccountRole.Owner || adminUser) {
realObjects.push(...realDocs)
} else {
realObjects.push(...realDocs.filter((p) => allPersonAccounts.has(p.createdBy)))
}
}
} else {
realObjects = objects
}
const ops = client.apply('delete')
for (const object of objects) {
for (const object of realObjects) {
if (client.getHierarchy().isDerived(object._class, core.class.AttachedDoc)) {
const adoc = object as AttachedDoc
await ops

View File

@ -521,7 +521,7 @@ export interface Action<T extends Doc = Doc, P = Record<string, any>> extends Do
// For example, it could be global action and action for focus class, second one fill override first one.
override?: Ref<Action>[]
// Avaible only for workspace owners
// Available only for workspace owners
secured?: boolean
allowedForEditableContent?: boolean
}

View File

@ -20,7 +20,7 @@
import notification, { DocNotifyContext, InboxNotification, inboxId } from '@hcengineering/notification'
import { BrowserNotificatator, InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
import { IntlString, broadcastEvent, getMetadata, getResource } from '@hcengineering/platform'
import { ActionContext, createQuery, getClient } from '@hcengineering/presentation'
import { ActionContext, createQuery, getClient, isAdminUser } from '@hcengineering/presentation'
import setting from '@hcengineering/setting'
import support, { SupportStatus, supportLink } from '@hcengineering/support'
import {
@ -633,7 +633,7 @@
}}
/>
</div>
{:else if employee?.active || account.role === AccountRole.Owner}
{:else if employee?.active || account.role === AccountRole.Owner || isAdminUser()}
<ActionHandler />
<svg class="svg-mask">
<clipPath id="notify-normal">

View File

@ -11,7 +11,7 @@ import core, {
} from '@hcengineering/core'
import login, { loginId } from '@hcengineering/login'
import { addEventListener, broadcastEvent, getMetadata, getResource, setMetadata } from '@hcengineering/platform'
import presentation, { closeClient, refreshClient, setClient } from '@hcengineering/presentation'
import presentation, { closeClient, purgeClient, refreshClient, setClient } from '@hcengineering/presentation'
import {
fetchMetadataLocalStorage,
getCurrentLocation,
@ -77,6 +77,8 @@ export async function connect (title: string): Promise<Client | undefined> {
}
if (_token !== token && _client !== undefined) {
// We need to flush all data from memory
await purgeClient()
await _client.close()
_client = undefined
}

View File

@ -24,6 +24,7 @@ import WorkbenchApp from './components/WorkbenchApp.svelte'
import { doNavigate } from './utils'
import Workbench from './components/Workbench.svelte'
import ServerManager from './components/ServerManager.svelte'
import { isAdminUser } from '@hcengineering/presentation'
async function hasArchiveSpaces (spaces: Space[]): Promise<boolean> {
return spaces.find((sp) => sp.archived) !== undefined
@ -51,7 +52,7 @@ export default async (): Promise<Resources> => ({
},
function: {
HasArchiveSpaces: hasArchiveSpaces,
IsOwner: async (docs: Space[]) => getCurrentAccount().role === AccountRole.Owner
IsOwner: async (docs: Space[]) => getCurrentAccount().role === AccountRole.Owner || isAdminUser()
},
actionImpl: {
Navigate: doNavigate

View File

@ -59,7 +59,7 @@ export class ConfigurationMiddleware extends BaseMiddleware implements Middlewar
if (this.targetDomains.includes(domain)) {
if (ctx.userEmail !== configurationAccountEmail) {
const account = await this.getUser(ctx)
if (account.role !== AccountRole.Owner) {
if (account.role !== AccountRole.Owner && ctx.admin !== true) {
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
}
}
@ -82,7 +82,7 @@ export class ConfigurationMiddleware extends BaseMiddleware implements Middlewar
if (this.targetDomains.includes(domain)) {
if (ctx.userEmail !== configurationAccountEmail) {
const account = await this.getUser(ctx)
if (account.role !== AccountRole.Owner && account._id !== core.account.System) {
if (account.role !== AccountRole.Owner && account._id !== core.account.System && ctx.admin !== true) {
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
}
}

View File

@ -313,7 +313,7 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar
const space = this.privateSpaces[tx.objectSpace]
if (space !== undefined) {
targets = await this.getTargets(space.members)
if (!isOwner(account)) {
if (!isOwner(account, ctx)) {
const cudTx = tx as TxCUD<Doc>
const isSpace = h.isDerived(cudTx.objectClass, core.class.Space)
const allowed = this.allowedSpaces[account._id]
@ -482,7 +482,7 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar
const field = this.getKey(_class)
if (!isSystem(account) && account.role !== AccountRole.Guest) {
if (!isOwner(account) || !this.storage.hierarchy.isDerived(_class, core.class.Space)) {
if (!isOwner(account, ctx) || !this.storage.hierarchy.isDerived(_class, core.class.Space)) {
if (query[field] !== undefined) {
;(newQuery as any)[field] = await this.mergeQuery(account, query[field], domain)
} else {
@ -492,7 +492,7 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar
}
}
const findResult = await this.provideFindAll(ctx, _class, newQuery, options)
if (!isOwner(account) && account.role !== AccountRole.Guest) {
if (!isOwner(account, ctx) && account.role !== AccountRole.Guest) {
if (options?.lookup !== undefined) {
for (const object of findResult) {
if (object.$lookup !== undefined) {
@ -521,7 +521,7 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar
async isUnavailable (ctx: SessionContext, space: Ref<Space>): Promise<boolean> {
if (this.privateSpaces[space] === undefined) return false
const account = await getUser(this.storage, ctx)
if (isOwner(account)) return false
if (isOwner(account, ctx)) return false
return !this.allowedSpaces[account._id]?.includes(space)
}

View File

@ -51,8 +51,8 @@ export async function getUser (storage: ServerStorage, ctx: SessionContext): Pro
return account
}
export function isOwner (account: Account): boolean {
return account.role === AccountRole.Owner || account._id === core.account.System
export function isOwner (account: Account, ctx: SessionContext): boolean {
return account.role === AccountRole.Owner || account._id === core.account.System || ctx.admin === true
}
export function isSystem (account: Account): boolean {