Skip to content

Commit

Permalink
Server: Allow enabling or disabling the sharing feature per user
Browse files Browse the repository at this point in the history
  • Loading branch information
laurent22 committed May 18, 2021
1 parent e6c4eb7 commit daaaa13
Show file tree
Hide file tree
Showing 11 changed files with 105 additions and 37 deletions.
Binary file modified packages/server/schema.sqlite
Binary file not shown.
2 changes: 2 additions & 0 deletions packages/server/src/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ export interface User extends WithDates, WithUuid {
full_name?: string;
is_admin?: number;
max_item_size?: number;
can_share?: number;
max_share_recipients?: number;
}

Expand Down Expand Up @@ -380,6 +381,7 @@ export const databaseSchema: DatabaseTables = {
updated_time: { type: 'string' },
created_time: { type: 'string' },
max_item_size: { type: 'number' },
can_share: { type: 'number' },
max_share_recipients: { type: 'number' },
},
sessions: {
Expand Down
12 changes: 12 additions & 0 deletions packages/server/src/migrations/20210518150551_can_share.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Knex } from 'knex';
import { DbConnection } from '../db';

export async function up(db: DbConnection): Promise<any> {
await db.schema.alterTable('users', function(table: Knex.CreateTableBuilder) {
table.integer('can_share').defaultTo(1).notNullable();
});
}

export async function down(_db: DbConnection): Promise<any> {

}
23 changes: 5 additions & 18 deletions packages/server/src/models/ItemModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,10 @@ import { ItemType, databaseSchema, Uuid, Item, ShareType, Share, ChangeType, Use
import { defaultPagination, paginateDbQuery, PaginatedResults, Pagination } from './utils/pagination';
import { isJoplinItemName, isJoplinResourceBlobPath, linkedResourceIds, serializeJoplinItem, unserializeJoplinItem } from '../utils/joplinUtils';
import { ModelType } from '@joplin/lib/BaseModel';
import { ApiError, ErrorForbidden, ErrorNotFound, ErrorPayloadTooLarge, ErrorUnprocessableEntity } from '../utils/errors';
import { ApiError, ErrorForbidden, ErrorNotFound, ErrorUnprocessableEntity } from '../utils/errors';
import { Knex } from 'knex';
import { ChangePreviousItem } from './ChangeModel';
import { _ } from '@joplin/lib/locale';

const prettyBytes = require('pretty-bytes');
const mimeUtils = require('@joplin/lib/mime-utils.js').mime;

// Converts "root:/myfile.txt:" to "myfile.txt"
Expand Down Expand Up @@ -291,16 +289,17 @@ export default class ItemModel extends BaseModel<Item> {

const isJoplinItem = isJoplinItemName(name);
let isNote = false;
let itemTitle = '';

const item: Item = {
name,
};

let joplinItem: any = null;

let resourceIds: string[] = [];

if (isJoplinItem) {
const joplinItem = await unserializeJoplinItem(buffer.toString());
joplinItem = await unserializeJoplinItem(buffer.toString());
isNote = joplinItem.type_ === ModelType.Note;
resourceIds = isNote ? linkedResourceIds(joplinItem.body) : [];

Expand All @@ -316,8 +315,6 @@ export default class ItemModel extends BaseModel<Item> {
delete joplinItem.type_;
delete joplinItem.encryption_applied;

itemTitle = joplinItem.title || '';

item.content = Buffer.from(JSON.stringify(joplinItem));
} else {
item.content = buffer;
Expand All @@ -327,17 +324,7 @@ export default class ItemModel extends BaseModel<Item> {

if (options.shareId) item.jop_share_id = options.shareId;

// If the item is encrypted, we apply a multipler because encrypted
// items can be much larger (seems to be up to twice the size but for
// safety let's go with 2.2).
const maxSize = user.max_item_size * (item.jop_encryption_applied ? 2.2 : 1);
if (maxSize && buffer.byteLength > maxSize) {
throw new ErrorPayloadTooLarge(_('Cannot save %s "%s" because it is larger than than the allowed limit (%s)',
isNote ? _('note') : _('attachment'),
itemTitle ? itemTitle : name,
prettyBytes(user.max_item_size)
));
}
await this.models().user().checkMaxItemSizeLimit(user, buffer, item, joplinItem);

return this.withTransaction<Item>(async () => {
const savedItem = await this.saveForUser(user.id, item);
Expand Down
2 changes: 2 additions & 0 deletions packages/server/src/models/ShareModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export default class ShareModel extends BaseModel<Share> {

public async checkIfAllowed(user: User, action: AclAction, resource: Share = null): Promise<void> {
if (action === AclAction.Create) {
if (!user.can_share) throw new ErrorForbidden('The sharing feature is not enabled for this account');

if (!await this.models().item().userHasItem(user.id, resource.item_id)) throw new ErrorForbidden('cannot share an item not owned by the user');

if (resource.type === ShareType.Folder) {
Expand Down
4 changes: 3 additions & 1 deletion packages/server/src/models/ShareUserModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ export default class ShareUserModel extends BaseModel<ShareUser> {

public async checkIfAllowed(user: User, action: AclAction, resource: ShareUser = null): Promise<void> {
if (action === AclAction.Create) {
const recipient = await this.models().user().load(resource.user_id, { fields: ['can_share'] });
if (!recipient.can_share) throw new ErrorForbidden('The sharing feature is not enabled for the recipient account');

const share = await this.models().share().load(resource.share_id);
if (share.owner_id !== user.id) throw new ErrorForbidden('no access to the share object');
if (share.owner_id === resource.user_id) throw new ErrorForbidden('cannot share an item with yourself');
Expand Down Expand Up @@ -96,7 +99,6 @@ export default class ShareUserModel extends BaseModel<ShareUser> {
}

public async addByEmail(shareId: Uuid, userEmail: string): Promise<ShareUser> {
// TODO: check that user can access this share
const share = await this.models().share().load(shareId);
if (!share) throw new ErrorNotFound(`No such share: ${shareId}`);

Expand Down
43 changes: 41 additions & 2 deletions packages/server/src/models/UserModel.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import BaseModel, { AclAction, SaveOptions, ValidateOptions } from './BaseModel';
import { User } from '../db';
import { Item, User } from '../db';
import * as auth from '../utils/auth';
import { ErrorUnprocessableEntity, ErrorForbidden } from '../utils/errors';
import { ErrorUnprocessableEntity, ErrorForbidden, ErrorPayloadTooLarge } from '../utils/errors';
import { ModelType } from '@joplin/lib/BaseModel';
import { _ } from '@joplin/lib/locale';
import prettyBytes = require('pretty-bytes');

export default class UserModel extends BaseModel<User> {

Expand Down Expand Up @@ -30,6 +33,7 @@ export default class UserModel extends BaseModel<User> {
if ('is_admin' in object) user.is_admin = object.is_admin;
if ('full_name' in object) user.full_name = object.full_name;
if ('max_item_size' in object) user.max_item_size = object.max_item_size;
if ('can_share' in object) user.can_share = object.can_share;

return user;
}
Expand Down Expand Up @@ -69,6 +73,41 @@ export default class UserModel extends BaseModel<User> {
}
}

public async checkMaxItemSizeLimit(user: User, buffer: Buffer, item: Item, joplinItem: any) {
const itemTitle = joplinItem ? joplinItem.title || '' : '';
const isNote = joplinItem && joplinItem.type_ === ModelType.Note;

// If the item is encrypted, we apply a multipler because encrypted
// items can be much larger (seems to be up to twice the size but for
// safety let's go with 2.2).
const maxSize = user.max_item_size * (item.jop_encryption_applied ? 2.2 : 1);
if (maxSize && buffer.byteLength > maxSize) {
throw new ErrorPayloadTooLarge(_('Cannot save %s "%s" because it is larger than than the allowed limit (%s)',
isNote ? _('note') : _('attachment'),
itemTitle ? itemTitle : name,
prettyBytes(user.max_item_size)
));
}
}

// public async checkCanShare(share:Share) {

// // const itemTitle = joplinItem ? joplinItem.title || '' : '';
// // const isNote = joplinItem && joplinItem.type_ === ModelType.Note;

// // // If the item is encrypted, we apply a multipler because encrypted
// // // items can be much larger (seems to be up to twice the size but for
// // // safety let's go with 2.2).
// // const maxSize = user.max_item_size * (item.jop_encryption_applied ? 2.2 : 1);
// // if (maxSize && buffer.byteLength > maxSize) {
// // throw new ErrorPayloadTooLarge(_('Cannot save %s "%s" because it is larger than than the allowed limit (%s)',
// // isNote ? _('note') : _('attachment'),
// // itemTitle ? itemTitle : name,
// // prettyBytes(user.max_item_size)
// // ));
// // }
// }

protected async validate(object: User, options: ValidateOptions = {}): Promise<User> {
const user: User = await super.validate(object, options);

Expand Down
47 changes: 31 additions & 16 deletions packages/server/src/routes/api/shares.folder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -793,21 +793,36 @@ describe('shares.folder', function() {
await expectHttpError(async () => postApi<Share>(session1.id, 'shares', { folder_id: '000000000000000000000000000000F2' }), ErrorForbidden.httpCode);
});

// test('should check permissions - only owner of share can deleted associated folder', async function() {
// const { session: session1 } = await createUserAndSession(1);
// const { session: session2 } = await createUserAndSession(2);

// await shareFolderWithUser(session1.id, session2.id, '000000000000000000000000000000F1', [
// {
// id: '000000000000000000000000000000F1',
// children: [
// {
// id: '00000000000000000000000000000001',
// },
// ],
// },
// ]);
// await expectHttpError(async () => deleteApi(session2.id, 'items/root:/000000000000000000000000000000F1.md:'), ErrorForbidden.httpCode);
// });
test('should check permissions - cannot share if share feature not enabled', async function() {
const { user: user1, session: session1 } = await createUserAndSession(1);
const { session: session2 } = await createUserAndSession(2);
await models().user().save({ id: user1.id, can_share: 0 });

await expectHttpError(async () =>
shareFolderWithUser(session1.id, session2.id, '000000000000000000000000000000F1', [
{
id: '000000000000000000000000000000F1',
children: [],
},
]),
ErrorForbidden.httpCode
);
});

test('should check permissions - cannot share if share feature not enabled for recipient', async function() {
const { session: session1 } = await createUserAndSession(1);
const { user: user2, session: session2 } = await createUserAndSession(2);
await models().user().save({ id: user2.id, can_share: 0 });

await expectHttpError(async () =>
shareFolderWithUser(session1.id, session2.id, '000000000000000000000000000000F1', [
{
id: '000000000000000000000000000000F1',
children: [],
},
]),
ErrorForbidden.httpCode
);
});

});
1 change: 1 addition & 0 deletions packages/server/src/routes/index/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ function makeUser(isNew: boolean, fields: any): User {
if ('full_name' in fields) user.full_name = fields.full_name;
if ('is_admin' in fields) user.is_admin = fields.is_admin;
if ('max_item_size' in fields) user.max_item_size = fields.max_item_size;
user.can_share = fields.can_share ? 1 : 0;

if (fields.password) {
if (fields.password !== fields.password2) throw new ErrorUnprocessableEntity('Passwords do not match');
Expand Down
6 changes: 6 additions & 0 deletions packages/server/src/views/index/user.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@
<input class="input" type="number" name="max_item_size" value="{{user.max_item_size}}"/>
</div>
</div>

<div class="field">
<label class="checkbox">
<input type="checkbox" name="can_share" {{#user.can_share}}checked{{/user.can_share}} value="1"> Can share
</label>
</div>
{{/global.owner.is_admin}}
<div class="field">
<label class="label">Password</label>
Expand Down
2 changes: 2 additions & 0 deletions packages/server/src/views/index/users.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<th>Full name</th>
<th>Email</th>
<th>Max Item Size</th>
<th>Can share</th>
<th>Is admin?</th>
<th>Actions</th>
</tr>
Expand All @@ -14,6 +15,7 @@
<td>{{full_name}}</td>
<td>{{email}}</td>
<td>{{formattedItemMaxSize}}</td>
<td>{{can_share}}</td>
<td>{{is_admin}}</td>
<td><a href="{{{global.baseUrl}}}/users/{{id}}" class="button is-primary is-small">Edit</a></td>
</tr>
Expand Down

0 comments on commit daaaa13

Please sign in to comment.