mirror of
https://github.com/hcengineering/platform.git
synced 2025-06-09 09:20:54 +00:00
Basic implementation of a readonly user role
Signed-off-by: Anton Alexeyev <alexeyev.anton@gmail.com>
This commit is contained in:
parent
46113c7135
commit
2fc09ab94f
@ -517,6 +517,7 @@ export interface Permission extends Doc {
|
||||
* @public
|
||||
*/
|
||||
export enum AccountRole {
|
||||
ReadOnlyGuest = 'READONLYGUEST',
|
||||
DocGuest = 'DocGuest',
|
||||
Guest = 'GUEST',
|
||||
User = 'USER',
|
||||
@ -528,6 +529,7 @@ export enum AccountRole {
|
||||
* @public
|
||||
*/
|
||||
export const roleOrder: Record<AccountRole, number> = {
|
||||
[AccountRole.ReadOnlyGuest]: 5,
|
||||
[AccountRole.DocGuest]: 10,
|
||||
[AccountRole.Guest]: 20,
|
||||
[AccountRole.User]: 30,
|
||||
|
@ -27,6 +27,7 @@
|
||||
const currentAccount = getCurrentAccount()
|
||||
|
||||
const items: DropdownIntlItem[] = [
|
||||
{ id: AccountRole.ReadOnlyGuest, label: setting.string.User },
|
||||
{ id: AccountRole.User, label: setting.string.User },
|
||||
{ id: AccountRole.Maintainer, label: setting.string.Maintainer },
|
||||
{ id: AccountRole.Owner, label: setting.string.Owner }
|
||||
|
@ -22,33 +22,33 @@
|
||||
Class,
|
||||
type CollaborativeDoc,
|
||||
type Doc,
|
||||
type Ref,
|
||||
generateId,
|
||||
getCurrentAccount,
|
||||
makeDocCollabId
|
||||
makeDocCollabId,
|
||||
type Ref
|
||||
} from '@hcengineering/core'
|
||||
import { IntlString, translate } from '@hcengineering/platform'
|
||||
import {
|
||||
DrawingCmd,
|
||||
KeyedAttribute,
|
||||
getAttribute,
|
||||
getClient,
|
||||
getFileUrl,
|
||||
getImageSize,
|
||||
imageSizeToRatio
|
||||
imageSizeToRatio,
|
||||
KeyedAttribute
|
||||
} from '@hcengineering/presentation'
|
||||
import { markupToJSON } from '@hcengineering/text'
|
||||
import {
|
||||
AnySvelteComponent,
|
||||
Button,
|
||||
getEventPositionElement,
|
||||
getPopupPositionElement,
|
||||
IconScribble,
|
||||
IconSize,
|
||||
Loading,
|
||||
PopupAlignment,
|
||||
ThrottledCaller,
|
||||
getEventPositionElement,
|
||||
getPopupPositionElement,
|
||||
themeStore
|
||||
themeStore,
|
||||
ThrottledCaller
|
||||
} from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
import { AnyExtension, Editor, FocusPosition, mergeAttributes } from '@tiptap/core'
|
||||
@ -83,7 +83,7 @@
|
||||
import { InlineCommentCollaborationExtension } from './extension/inlineComment'
|
||||
import { LeftMenuExtension } from './extension/leftMenu'
|
||||
import { mermaidOptions } from './extension/mermaid'
|
||||
import { ReferenceExtension, referenceConfig } from './extension/reference'
|
||||
import { referenceConfig, ReferenceExtension } from './extension/reference'
|
||||
import { type FileAttachFunction } from './extension/types'
|
||||
import { inlineCommandsConfig } from './extensions'
|
||||
|
||||
@ -121,7 +121,7 @@
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const account = getCurrentAccount()
|
||||
$: isGuest = account.role === AccountRole.DocGuest
|
||||
$: isGuest = account.role === AccountRole.DocGuest || account.role === AccountRole.ReadOnlyGuest
|
||||
|
||||
const objectClass = object._class
|
||||
const objectId = object._id
|
||||
@ -142,7 +142,7 @@
|
||||
let remoteSynced = false
|
||||
|
||||
$: loading = !localSynced && !remoteSynced
|
||||
$: editable = !readonly && !contentError && remoteSynced
|
||||
$: editable = !readonly && !contentError && remoteSynced && account.role !== AccountRole.ReadOnlyGuest
|
||||
|
||||
void localProvider.loaded.then(() => (localSynced = true))
|
||||
void remoteProvider.loaded.then(() => (remoteSynced = true))
|
||||
|
@ -75,6 +75,8 @@ async function createMessages (
|
||||
for (const data of result) {
|
||||
void api.event(
|
||||
{
|
||||
// TODO: We should decide what to do with communications package and remove this workaround
|
||||
// @ts-expect-error Temporary workaround because of Communications package depends on npm core package
|
||||
account: systemAccount
|
||||
},
|
||||
{
|
||||
|
@ -412,6 +412,8 @@ async function updateCollaborators (control: TriggerControl, ctx: TxCreateDoc<Ca
|
||||
|
||||
if (collaborators.length === 0) continue
|
||||
void communicationApi.event(
|
||||
// TODO: We should decide what to do with communications package and remove this workaround
|
||||
// @ts-expect-error Temporary workaround because of Communications package depends on npm core package
|
||||
{ account: systemAccount },
|
||||
{
|
||||
type: NotificationRequestEventType.AddCollaborators,
|
||||
|
@ -26,7 +26,8 @@ export function getMigrations (ns: string): [string, string][] {
|
||||
getV6Migration(ns),
|
||||
getV7Migration(ns),
|
||||
getV8Migration(ns),
|
||||
getV9Migration(ns)
|
||||
getV9Migration(ns),
|
||||
getV10Migration(ns)
|
||||
]
|
||||
}
|
||||
|
||||
@ -45,7 +46,7 @@ function getV1Migration (ns: string): [string, string] {
|
||||
/* ======= T Y P E S ======= */
|
||||
CREATE TYPE IF NOT EXISTS ${ns}.social_id_type AS ENUM ('email', 'github', 'google', 'phone', 'oidc', 'huly', 'telegram');
|
||||
CREATE TYPE IF NOT EXISTS ${ns}.location AS ENUM ('kv', 'weur', 'eeur', 'wnam', 'enam', 'apac');
|
||||
CREATE TYPE IF NOT EXISTS ${ns}.workspace_role AS ENUM ('OWNER', 'MAINTAINER', 'USER', 'GUEST', 'DOCGUEST');
|
||||
CREATE TYPE IF NOT EXISTS ${ns}.workspace_role AS ENUM ('OWNER', 'MAINTAINER', 'USER', 'GUEST', 'DOCGUEST', 'READONLYGUEST');
|
||||
|
||||
/* ======= P E R S O N ======= */
|
||||
CREATE TABLE IF NOT EXISTS ${ns}.person (
|
||||
@ -341,3 +342,12 @@ function getV9Migration (ns: string): [string, string] {
|
||||
`
|
||||
]
|
||||
}
|
||||
|
||||
function getV10Migration (ns: string): [string, string] {
|
||||
return [
|
||||
'account_db_v10_add_readonly_role',
|
||||
`
|
||||
ALTER TYPE ${ns}.workspace_role ADD VALUE 'READONLYGUEST' AFTER 'DOCGUEST';
|
||||
`
|
||||
]
|
||||
}
|
||||
|
@ -21,16 +21,16 @@ import {
|
||||
groupByArray,
|
||||
isActiveMode,
|
||||
MeasureContext,
|
||||
type Person,
|
||||
type PersonId,
|
||||
type PersonUuid,
|
||||
roleOrder,
|
||||
SocialIdType,
|
||||
SocialKey,
|
||||
systemAccountUuid,
|
||||
type WorkspaceInfoWithStatus as WorkspaceInfoWithStatusCore,
|
||||
WorkspaceMode,
|
||||
WorkspaceUuid,
|
||||
type Person,
|
||||
type PersonId,
|
||||
type PersonUuid,
|
||||
type WorkspaceInfoWithStatus as WorkspaceInfoWithStatusCore
|
||||
WorkspaceUuid
|
||||
} from '@hcengineering/core'
|
||||
import { getMongoClient } from '@hcengineering/mongo' // TODO: get rid of this import later
|
||||
import platform, { getMetadata, PlatformError, Severity, Status, translate } from '@hcengineering/platform'
|
||||
@ -45,21 +45,21 @@ import { MongoAccountDB } from './collections/mongo'
|
||||
import { PostgresAccountDB } from './collections/postgres/postgres'
|
||||
import { accountPlugin } from './plugin'
|
||||
import {
|
||||
type Account,
|
||||
type AccountDB,
|
||||
AccountEventType,
|
||||
AccountMethodHandler,
|
||||
Integration,
|
||||
LoginInfo,
|
||||
Meta,
|
||||
OtpInfo,
|
||||
type RegionInfo,
|
||||
type SocialId,
|
||||
type Workspace,
|
||||
WorkspaceInfoWithStatus,
|
||||
WorkspaceInvite,
|
||||
WorkspaceLoginInfo,
|
||||
WorkspaceStatus,
|
||||
type Account,
|
||||
type AccountDB,
|
||||
type RegionInfo,
|
||||
type SocialId,
|
||||
type Workspace
|
||||
WorkspaceStatus
|
||||
} from './types'
|
||||
import { isAdminEmail } from './admin'
|
||||
|
||||
@ -112,6 +112,14 @@ export function getRolePower (role: AccountRole): number {
|
||||
return roleOrder[role]
|
||||
}
|
||||
|
||||
export function isReadOnlyOrGuest (account: AccountUuid, extra: Record<string, any> | undefined): boolean {
|
||||
return isGuest(account, extra) || extra?.readonly === 'true'
|
||||
}
|
||||
|
||||
export function isGuest (account: AccountUuid, extra: Record<string, any> | undefined): boolean {
|
||||
return account === GUEST_ACCOUNT && extra?.guest === 'true'
|
||||
}
|
||||
|
||||
export function wrap (
|
||||
accountMethod: (ctx: MeasureContext, db: AccountDB, branding: Branding | null, ...args: any[]) => Promise<any>
|
||||
): AccountMethodHandler {
|
||||
@ -532,7 +540,7 @@ export async function selectWorkspace (
|
||||
meta?: Meta
|
||||
): Promise<WorkspaceLoginInfo> {
|
||||
const { workspaceUrl, kind, externalRegions = [] } = params
|
||||
const { account: accountUuid, workspace: tokenWorkspaceUuid, extra } = decodeTokenVerbose(ctx, token ?? '')
|
||||
let { account: accountUuid, workspace: tokenWorkspaceUuid, extra } = decodeTokenVerbose(ctx, token ?? '')
|
||||
const getKind = (region: string | undefined): EndpointKind => {
|
||||
switch (kind) {
|
||||
case 'external':
|
||||
@ -546,7 +554,7 @@ export async function selectWorkspace (
|
||||
}
|
||||
}
|
||||
|
||||
if (accountUuid === GUEST_ACCOUNT && extra?.guest === 'true') {
|
||||
if (isGuest(accountUuid, extra)) {
|
||||
const workspace = await getWorkspaceByUrl(db, workspaceUrl)
|
||||
if (workspace == null) {
|
||||
ctx.error('Workspace not found in selectWorkspace', { workspaceUrl, kind, accountUuid, extra })
|
||||
@ -605,6 +613,13 @@ export async function selectWorkspace (
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
|
||||
}
|
||||
|
||||
if (role === AccountRole.ReadOnlyGuest) {
|
||||
if (extra == null) {
|
||||
extra = {}
|
||||
}
|
||||
extra.readonly = 'true'
|
||||
}
|
||||
|
||||
const wsStatus = await db.workspaceStatus.findOne({ workspaceUuid: workspace.uuid })
|
||||
|
||||
if (wsStatus != null) {
|
||||
|
@ -17,9 +17,10 @@ import { decodeDocumentId } from '@hcengineering/collaborator-client'
|
||||
import { MeasureContext } from '@hcengineering/core'
|
||||
import { decodeToken } from '@hcengineering/server-token'
|
||||
import { Extension, onAuthenticatePayload } from '@hocuspocus/server'
|
||||
import { isReadOnlyOrGuest } from '@hcengineering/account'
|
||||
|
||||
import { Context, buildContext } from '../context'
|
||||
import { getWorkspaceIds, isGuest } from '../utils'
|
||||
import { getWorkspaceIds } from '../utils'
|
||||
|
||||
export interface AuthenticationConfiguration {
|
||||
ctx: MeasureContext
|
||||
@ -38,7 +39,7 @@ export class AuthenticationExtension implements Extension {
|
||||
|
||||
return await ctx.with('authenticate', { workspaceId }, async () => {
|
||||
const token = decodeToken(data.token)
|
||||
const readonly = isGuest(token)
|
||||
const readonly = isReadOnlyOrGuest(token.account, token.extra)
|
||||
|
||||
ctx.info('authenticate', {
|
||||
workspaceId,
|
||||
|
@ -12,17 +12,11 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
import { GUEST_ACCOUNT } from '@hcengineering/account'
|
||||
import { getClient as getAccountClient } from '@hcengineering/account-client'
|
||||
import { type WorkspaceIds } from '@hcengineering/core'
|
||||
import { type Token } from '@hcengineering/server-token'
|
||||
|
||||
import config from './config'
|
||||
|
||||
export function isGuest (token: Token): boolean {
|
||||
return token.account === GUEST_ACCOUNT && token.extra?.guest === 'true'
|
||||
}
|
||||
|
||||
// TODO: consider storing this in a cache for some short period of time
|
||||
export async function getWorkspaceIds (token: string): Promise<WorkspaceIds> {
|
||||
const workspaceInfo = await getAccountClient(config.AccountsUrl, token).getWorkspaceInfo()
|
||||
|
@ -32,7 +32,7 @@ import core, {
|
||||
TypedSpace,
|
||||
type MeasureContext,
|
||||
type SessionData,
|
||||
type AccountUuid
|
||||
type AccountUuid, AccountRole
|
||||
} from '@hcengineering/core'
|
||||
import platform, { PlatformError, Severity, Status } from '@hcengineering/platform'
|
||||
import { Middleware, TxMiddlewareResult, type PipelineContext } from '@hcengineering/server-core'
|
||||
@ -337,6 +337,11 @@ export class SpacePermissionsMiddleware extends BaseMiddleware implements Middle
|
||||
}
|
||||
|
||||
protected checkPermissions (ctx: MeasureContext, tx: Tx): void {
|
||||
const account = ctx.contextData.account
|
||||
if (account.role === AccountRole.ReadOnlyGuest) {
|
||||
this.throwForbidden()
|
||||
}
|
||||
|
||||
if (tx._class === core.class.TxApplyIf) {
|
||||
const applyTx = tx as TxApplyIf
|
||||
|
||||
|
@ -450,6 +450,8 @@ export class ClientSession implements Session {
|
||||
private getCommunicationCtx (): CommunicationSession {
|
||||
return {
|
||||
sessionId: this.sessionId,
|
||||
// TODO: We should decide what to do with communications package and remove this workaround
|
||||
// @ts-expect-error Temporary workaround because of Communications package depends on npm core package
|
||||
account: this.account
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user