refactor(server): permission (#10449)

This commit is contained in:
liuyi
2025-03-05 15:57:00 +08:00
committed by GitHub
parent bf7b1646b3
commit 61162c59fc
61 changed files with 2680 additions and 3562 deletions
@@ -0,0 +1,160 @@
import { PrismaClient } from '@prisma/client';
import test from 'ava';
import Sinon from 'sinon';
import { EventBus } from '../../base';
import { DocRole, Models } from '../../models';
import { createTestingModule, TestingModule } from '../utils';
let db: PrismaClient;
let models: Models;
let module: TestingModule;
test.before(async () => {
module = await createTestingModule({
tapModule: m => {
m.overrideProvider(EventBus).useValue(Sinon.createStubInstance(EventBus));
},
});
models = module.get(Models);
db = module.get(PrismaClient);
});
test.beforeEach(async () => {
await module.initTestingDB();
Sinon.reset();
});
test.after(async () => {
await module.close();
});
async function create() {
return db.workspace.create({
data: { public: false },
});
}
test('should set doc owner', async t => {
const workspace = await create();
const user = await models.user.create({ email: 'u1@affine.pro' });
const docId = 'fake-doc-id';
await models.docUser.setOwner(workspace.id, docId, user.id);
const role = await models.docUser.get(workspace.id, docId, user.id);
t.is(role?.type, DocRole.Owner);
});
test('should transfer doc owner', async t => {
const user = await models.user.create({ email: 'u1@affine.pro' });
const user2 = await models.user.create({ email: 'u2@affine.pro' });
const workspace = await create();
const docId = 'fake-doc-id';
await models.docUser.setOwner(workspace.id, docId, user.id);
await models.docUser.setOwner(workspace.id, docId, user2.id);
const oldOwnerRole = await models.docUser.get(workspace.id, docId, user.id);
const newOwnerRole = await models.docUser.get(workspace.id, docId, user2.id);
t.is(oldOwnerRole?.type, DocRole.Manager);
t.is(newOwnerRole?.type, DocRole.Owner);
});
test('should set doc user role', async t => {
const workspace = await create();
const user = await models.user.create({ email: 'u1@affine.pro' });
const docId = 'fake-doc-id';
await models.docUser.set(workspace.id, docId, user.id, DocRole.Manager);
const role = await models.docUser.get(workspace.id, docId, user.id);
t.is(role?.type, DocRole.Manager);
});
test('should not allow setting doc owner through setDocUserRole', async t => {
const workspace = await create();
const user = await models.user.create({ email: 'u1@affine.pro' });
const docId = 'fake-doc-id';
await t.throwsAsync(
models.docUser.set(workspace.id, docId, user.id, DocRole.Owner),
{ message: 'Cannot set Owner role of a doc to a user.' }
);
});
test('should delete doc user role', async t => {
const workspace = await create();
const user = await models.user.create({ email: 'u1@affine.pro' });
const docId = 'fake-doc-id';
await models.docUser.set(workspace.id, docId, user.id, DocRole.Manager);
await models.docUser.delete(workspace.id, docId, user.id);
const role = await models.docUser.get(workspace.id, docId, user.id);
t.is(role, null);
});
test('should paginate doc user roles', async t => {
const workspace = await create();
const docId = 'fake-doc-id';
await db.user.createMany({
data: Array.from({ length: 200 }, (_, i) => ({
id: String(i),
name: `u${i}`,
email: `${i}@affine.pro`,
})),
});
await db.workspaceDocUserRole.createMany({
data: Array.from({ length: 200 }, (_, i) => ({
workspaceId: workspace.id,
docId,
userId: String(i),
type: DocRole.Editor,
createdAt: new Date(Date.now() + i * 1000),
})),
});
const [roles, total] = await models.docUser.paginate(workspace.id, docId, {
first: 10,
offset: 0,
});
t.is(roles.length, 10);
t.is(total, 200);
const [roles2] = await models.docUser.paginate(workspace.id, docId, {
after: roles.at(-1)?.createdAt.toISOString(),
first: 50,
offset: 0,
});
t.is(roles2.length, 50);
t.not(roles2[0].type, DocRole.Owner);
t.deepEqual(
roles2.map(r => r.userId),
roles2
.toSorted((a, b) => a.createdAt.getTime() - b.createdAt.getTime())
.map(r => r.userId)
);
});
test('should count doc user roles', async t => {
const workspace = await create();
const docId = 'fake-doc-id';
const users = await Promise.all([
models.user.create({ email: 'u1@affine.pro' }),
models.user.create({ email: 'u2@affine.pro' }),
]);
await Promise.all(
users.map(user =>
models.docUser.set(workspace.id, docId, user.id, DocRole.Manager)
)
);
const count = await models.docUser.count(workspace.id, docId);
t.is(count, 2);
});
@@ -1,232 +0,0 @@
import { PrismaClient } from '@prisma/client';
import ava, { TestFn } from 'ava';
import { Config } from '../../base/config';
import { WorkspaceRole } from '../../core/permission';
import { PublicPageMode } from '../../models/common';
import { PageModel } from '../../models/page';
import { type User, UserModel } from '../../models/user';
import { type Workspace, WorkspaceModel } from '../../models/workspace';
import { createTestingModule, type TestingModule } from '../utils';
interface Context {
config: Config;
module: TestingModule;
db: PrismaClient;
user: UserModel;
workspace: WorkspaceModel;
page: PageModel;
}
const test = ava as TestFn<Context>;
test.before(async t => {
const module = await createTestingModule();
t.context.user = module.get(UserModel);
t.context.workspace = module.get(WorkspaceModel);
t.context.page = module.get(PageModel);
t.context.db = module.get(PrismaClient);
t.context.config = module.get(Config);
t.context.module = module;
});
let user: User;
let workspace: Workspace;
test.beforeEach(async t => {
await t.context.module.initTestingDB();
user = await t.context.user.create({
email: 'test@affine.pro',
});
workspace = await t.context.workspace.create(user.id);
});
test.after(async t => {
await t.context.module.close();
});
test('should create page with default mode and public false', async t => {
const page = await t.context.page.upsert(workspace.id, 'page1');
t.is(page.workspaceId, workspace.id);
t.is(page.docId, 'page1');
t.is(page.mode, PublicPageMode.Page);
t.is(page.public, false);
});
test('should update page', async t => {
const page = await t.context.page.upsert(workspace.id, 'page1');
const data = {
mode: PublicPageMode.Edgeless,
public: true,
};
await t.context.page.upsert(workspace.id, 'page1', data);
const page1 = await t.context.page.get(workspace.id, 'page1');
t.deepEqual(page1, {
...page,
...data,
});
});
test('should get null when page not exists', async t => {
const page = await t.context.page.get(workspace.id, 'page1');
t.is(page, null);
});
test('should get page by id and public flag', async t => {
await t.context.page.upsert(workspace.id, 'page1');
await t.context.page.upsert(workspace.id, 'page2', {
public: true,
});
let page1 = await t.context.page.get(workspace.id, 'page1');
t.is(page1!.public, false);
page1 = await t.context.page.get(workspace.id, 'page1', true);
t.is(page1, null);
let page2 = await t.context.page.get(workspace.id, 'page2', true);
t.is(page2!.public, true);
page2 = await t.context.page.get(workspace.id, 'page2', false);
t.is(page2, null);
});
test('should get public page count', async t => {
await t.context.page.upsert(workspace.id, 'page1', {
public: true,
});
await t.context.page.upsert(workspace.id, 'page2', {
public: true,
});
await t.context.page.upsert(workspace.id, 'page3');
const count = await t.context.page.getPublicsCount(workspace.id);
t.is(count, 2);
});
test('should get public pages of a workspace', async t => {
await t.context.page.upsert(workspace.id, 'page1', {
public: true,
});
await t.context.page.upsert(workspace.id, 'page2', {
public: true,
});
await t.context.page.upsert(workspace.id, 'page3');
const pages = await t.context.page.findPublics(workspace.id);
t.is(pages.length, 2);
t.deepEqual(pages.map(p => p.docId).sort(), ['page1', 'page2']);
});
test('should grant a member to access a page', async t => {
await t.context.page.upsert(workspace.id, 'page1', {
public: true,
});
const member = await t.context.user.create({
email: 'test1@affine.pro',
});
await t.context.page.grantMember(workspace.id, 'page1', member.id);
let hasAccess = await t.context.page.isMember(
workspace.id,
'page1',
member.id
);
t.true(hasAccess);
hasAccess = await t.context.page.isMember(
workspace.id,
'page1',
user.id,
WorkspaceRole.Collaborator
);
t.false(hasAccess);
// grant write permission
await t.context.page.grantMember(
workspace.id,
'page1',
user.id,
WorkspaceRole.Collaborator
);
hasAccess = await t.context.page.isMember(
workspace.id,
'page1',
user.id,
WorkspaceRole.Collaborator
);
t.true(hasAccess);
hasAccess = await t.context.page.isMember(
workspace.id,
'page1',
user.id,
WorkspaceRole.Collaborator
);
t.true(hasAccess);
// delete member
const count = await t.context.page.deleteMember(
workspace.id,
'page1',
user.id
);
t.is(count, 1);
hasAccess = await t.context.page.isMember(workspace.id, 'page1', user.id);
t.false(hasAccess);
});
test('should change the page owner', async t => {
await t.context.page.upsert(workspace.id, 'page1', {
public: true,
});
await t.context.page.grantMember(
workspace.id,
'page1',
user.id,
WorkspaceRole.Owner
);
t.true(
await t.context.page.isMember(
workspace.id,
'page1',
user.id,
WorkspaceRole.Owner
)
);
// change owner
const otherUser = await t.context.user.create({
email: 'test1@affine.pro',
});
await t.context.page.grantMember(
workspace.id,
'page1',
otherUser.id,
WorkspaceRole.Owner
);
t.true(
await t.context.page.isMember(
workspace.id,
'page1',
otherUser.id,
WorkspaceRole.Owner
)
);
t.false(
await t.context.page.isMember(
workspace.id,
'page1',
user.id,
WorkspaceRole.Owner
)
);
});
test('should not delete owner from page', async t => {
await t.context.page.upsert(workspace.id, 'page1', {
public: true,
});
await t.context.page.grantMember(
workspace.id,
'page1',
user.id,
WorkspaceRole.Owner
);
const count = await t.context.page.deleteMember(
workspace.id,
'page1',
user.id
);
t.is(count, 0);
});
@@ -2,13 +2,13 @@ import ava, { TestFn } from 'ava';
import Sinon from 'sinon';
import { EmailAlreadyUsed, EventBus } from '../../base';
import { WorkspaceRole } from '../../core/permission';
import { Models } from '../../models';
import { UserModel } from '../../models/user';
import { WorkspaceMemberStatus } from '../../models/workspace';
import { createTestingModule, sleep, type TestingModule } from '../utils';
interface Context {
module: TestingModule;
models: Models;
user: UserModel;
}
@@ -18,6 +18,7 @@ test.before(async t => {
const module = await createTestingModule({});
t.context.user = module.get(UserModel);
t.context.models = module.get(Models);
t.context.module = module;
});
@@ -253,24 +254,13 @@ test('should trigger user.deleted event', async t => {
const user = await t.context.user.create({
email: 'test@affine.pro',
workspacePermissions: {
create: {
workspace: {
create: {
id: 'test-workspace',
public: false,
},
},
type: WorkspaceRole.Owner,
status: WorkspaceMemberStatus.Accepted,
},
},
});
const workspace = await t.context.models.workspace.create(user.id);
await t.context.user.delete(user.id);
t.true(
spy.calledOnceWithExactly({ ...user, ownedWorkspaces: ['test-workspace'] })
spy.calledOnceWithExactly({ ...user, ownedWorkspaces: [workspace.id] })
);
// await for 'user.deleted' event to be emitted and executed
// avoid race condition cause database dead lock
@@ -0,0 +1,255 @@
import { PrismaClient } from '@prisma/client';
import test from 'ava';
import Sinon from 'sinon';
import { EventBus } from '../../base';
import { Models, WorkspaceMemberStatus, WorkspaceRole } from '../../models';
import { createTestingModule, TestingModule } from '../utils';
let db: PrismaClient;
let models: Models;
let module: TestingModule;
let event: Sinon.SinonStubbedInstance<EventBus>;
test.before(async () => {
module = await createTestingModule({
tapModule: m => {
m.overrideProvider(EventBus).useValue(Sinon.createStubInstance(EventBus));
},
});
models = module.get(Models);
event = module.get(EventBus);
db = module.get(PrismaClient);
});
test.beforeEach(async () => {
await module.initTestingDB();
Sinon.reset();
});
test.after(async () => {
await module.close();
});
async function create() {
return db.workspace.create({
data: { public: false },
});
}
test('should set workspace owner', async t => {
const workspace = await create();
const user = await models.user.create({ email: 'u1@affine.pro' });
await models.workspaceUser.setOwner(workspace.id, user.id);
const owner = await models.workspaceUser.getOwner(workspace.id);
t.is(owner.id, user.id);
});
test('should transfer workespace owner', async t => {
const user = await models.user.create({ email: 'u1@affine.pro' });
const user2 = await models.user.create({ email: 'u2@affine.pro' });
const workspace = await models.workspace.create(user.id);
await models.workspaceUser.setOwner(workspace.id, user2.id);
t.true(
event.emit.lastCall.calledWith('workspace.owner.changed', {
workspaceId: workspace.id,
from: user.id,
to: user2.id,
})
);
const owner2 = await models.workspaceUser.getOwner(workspace.id);
t.is(owner2.id, user2.id);
});
test('should get user role', async t => {
const workspace = await create();
const user = await models.user.create({ email: 'u1@affine.pro' });
await models.workspaceUser.set(workspace.id, user.id, WorkspaceRole.Admin);
const role = await models.workspaceUser.get(workspace.id, user.id);
t.is(role!.type, WorkspaceRole.Admin);
});
test('should get active workspace role', async t => {
const workspace = await create();
const user = await models.user.create({ email: 'u1@affine.pro' });
await models.workspaceUser.set(
workspace.id,
user.id,
WorkspaceRole.Admin,
WorkspaceMemberStatus.Accepted
);
const role = await models.workspaceUser.getActive(workspace.id, user.id);
t.is(role!.type, WorkspaceRole.Admin);
});
test('should not get inactive workspace role', async t => {
const workspace = await create();
const u1 = await models.user.create({ email: 'u1@affine.pro' });
await models.workspaceUser.set(workspace.id, u1.id, WorkspaceRole.Admin);
let role = await models.workspaceUser.getActive(workspace.id, u1.id);
t.is(role, null);
await models.workspaceUser.setStatus(
workspace.id,
u1.id,
WorkspaceMemberStatus.UnderReview
);
role = await models.workspaceUser.getActive(workspace.id, u1.id);
t.is(role, null);
});
test('should update user role', async t => {
const workspace = await create();
const user = await models.user.create({ email: 'u1@affine.pro' });
await models.workspaceUser.set(
workspace.id,
user.id,
WorkspaceRole.Admin,
WorkspaceMemberStatus.Accepted
);
const role = await models.workspaceUser.get(workspace.id, user.id);
t.is(role!.type, WorkspaceRole.Admin);
await models.workspaceUser.set(
workspace.id,
user.id,
WorkspaceRole.Collaborator
);
const role2 = await models.workspaceUser.get(workspace.id, user.id);
t.is(role2!.type, WorkspaceRole.Collaborator);
t.deepEqual(event.emit.lastCall.args, [
'workspace.members.roleChanged',
{
userId: user.id,
workspaceId: workspace.id,
role: WorkspaceRole.Collaborator,
},
]);
});
test('should return workspace role if status is Accepted', async t => {
const workspace = await create();
const u1 = await models.user.create({ email: 'u1@affine.pro' });
await models.workspaceUser.set(workspace.id, u1.id, WorkspaceRole.Admin);
await models.workspaceUser.setStatus(
workspace.id,
u1.id,
WorkspaceMemberStatus.Accepted
);
const role = await models.workspaceUser.get(workspace.id, u1.id);
t.is(role!.type, WorkspaceRole.Admin);
});
test('should delete workspace user role', async t => {
const workspace = await create();
const u1 = await models.user.create({ email: 'u1@affine.pro' });
await models.workspaceUser.set(workspace.id, u1.id, WorkspaceRole.Admin);
await models.workspaceUser.setStatus(
workspace.id,
u1.id,
WorkspaceMemberStatus.Accepted
);
let role = await models.workspaceUser.get(workspace.id, u1.id);
t.is(role!.type, WorkspaceRole.Admin);
await models.workspaceUser.delete(workspace.id, u1.id);
role = await models.workspaceUser.get(workspace.id, u1.id);
t.is(role, null);
});
test('should get user workspace roles with filter', async t => {
const ws1 = await create();
const ws2 = await create();
const user = await models.user.create({ email: 'u1@affine.pro' });
await db.workspaceUserRole.createMany({
data: [
{
workspaceId: ws1.id,
userId: user.id,
type: WorkspaceRole.Admin,
status: WorkspaceMemberStatus.Accepted,
},
{
workspaceId: ws2.id,
userId: user.id,
type: WorkspaceRole.Collaborator,
status: WorkspaceMemberStatus.Accepted,
},
],
});
let roles = await models.workspaceUser.getUserActiveRoles(user.id, {
role: WorkspaceRole.Admin,
});
t.is(roles.length, 1);
t.is(roles[0].type, WorkspaceRole.Admin);
roles = await models.workspaceUser.getUserActiveRoles(user.id);
t.is(roles.length, 2);
});
test('should paginate workspace user roles', async t => {
const workspace = await create();
await db.user.createMany({
data: Array.from({ length: 200 }, (_, i) => ({
id: String(i),
name: `u${i}`,
email: `${i}@affine.pro`,
})),
});
await db.workspaceUserRole.createMany({
data: Array.from({ length: 200 }, (_, i) => ({
workspaceId: workspace.id,
userId: String(i),
type: WorkspaceRole.Collaborator,
status: Object.values(WorkspaceMemberStatus)[
Math.floor(Math.random() * Object.values(WorkspaceMemberStatus).length)
],
createdAt: new Date(Date.now() + i * 1000),
})),
});
const [roles, total] = await models.workspaceUser.paginate(workspace.id, {
first: 10,
offset: 0,
});
t.is(roles.length, 10);
t.is(total, 200);
const [roles2] = await models.workspaceUser.paginate(workspace.id, {
after: roles.at(-1)?.createdAt.toISOString(),
first: 50,
offset: 0,
});
t.is(roles2.length, 50);
t.deepEqual(
roles2.map(r => r.id),
roles2
.toSorted((a, b) => a.createdAt.getTime() - b.createdAt.getTime())
.map(r => r.id)
);
});
File diff suppressed because it is too large Load Diff