🔐 Initial change on the public links using anonymous users (#69)
* Initial change on the public links using anonymous users Co-authored-by: Anton Shepilov <anton.shepilov@gmail.com>
This commit is contained in:
@@ -28,6 +28,7 @@ export default interface AuthServiceAPI extends TdriveServiceProvider {
|
||||
track: boolean;
|
||||
provider_id: string;
|
||||
application_id?: string;
|
||||
public_token_document_id?: string;
|
||||
} & any,
|
||||
): AccessToken;
|
||||
}
|
||||
|
||||
@@ -31,7 +31,12 @@ export class AuthService implements AuthServiceAPI {
|
||||
generateJWT(
|
||||
userId: uuid,
|
||||
email: string,
|
||||
options: { track: boolean; provider_id: string; application_id?: string },
|
||||
options: {
|
||||
track: boolean;
|
||||
provider_id: string;
|
||||
application_id?: string;
|
||||
public_token_document_id?: string;
|
||||
},
|
||||
): AccessToken {
|
||||
const now = Math.round(new Date().getTime() / 1000); // Current time in UTC
|
||||
assert(this.configuration.expiration, "jwt.expiration is missing");
|
||||
@@ -53,6 +58,7 @@ export class AuthService implements AuthServiceAPI {
|
||||
track: !!options.track,
|
||||
provider_id: options.provider_id || "",
|
||||
application_id: options.application_id,
|
||||
public_token_document_id: options.public_token_document_id,
|
||||
} as JwtType),
|
||||
refresh: this.sign({
|
||||
exp: jwtRefreshExpiration,
|
||||
@@ -64,6 +70,7 @@ export class AuthService implements AuthServiceAPI {
|
||||
track: !!options.track,
|
||||
provider_id: options.provider_id || "",
|
||||
application_id: options.application_id,
|
||||
public_token_document_id: options.public_token_document_id,
|
||||
} as JwtType),
|
||||
type: "Bearer",
|
||||
};
|
||||
|
||||
@@ -22,6 +22,7 @@ const jwtPlugin: FastifyPluginCallback = (fastify, _opts, next) => {
|
||||
...{ application_id: jwt.application_id || null },
|
||||
...{ server_request: jwt.server_request || false },
|
||||
...{ allow_tracking: jwt.track || false },
|
||||
...{ public_token_document_id: jwt.public_token_document_id || null },
|
||||
};
|
||||
request.log.debug(`Authenticated as user ${request.currentUser.id}`);
|
||||
};
|
||||
|
||||
@@ -125,7 +125,7 @@ export default class EmailPusherClass
|
||||
this.logger.info("email sent");
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error("Failed to send email", error);
|
||||
this.logger.error({ error: `${error}` }, "Failed to send email");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import { TransportRequestOptions } from "@elastic/elasticsearch/lib/Transport";
|
||||
import { logger } from "../../../../../../core/platform/framework/logger";
|
||||
import { EntityTarget, FindFilter, FindOptions, getEntityDefinition } from "../../api";
|
||||
import { asciiFold } from "../utils";
|
||||
import { comparisonType } from "src/core/platform/services/database/services/orm/repository/repository";
|
||||
import { comparisonType } from "../../../../../../core/platform/services/database/services/orm/repository/repository";
|
||||
|
||||
export function buildSearchQuery<Entity>(
|
||||
entityType: EntityTarget<Entity>,
|
||||
|
||||
@@ -9,4 +9,5 @@ export type JwtType = {
|
||||
refresh_nbf?: number;
|
||||
iat: number;
|
||||
track: boolean;
|
||||
public_token_document_id?: string;
|
||||
};
|
||||
|
||||
@@ -126,6 +126,7 @@ export class ConsoleController {
|
||||
{
|
||||
track: request.currentUser?.allow_tracking || false,
|
||||
provider_id: request.currentUser.identity_provider_id,
|
||||
public_token_document_id: request.currentUser.public_token_document_id,
|
||||
},
|
||||
),
|
||||
};
|
||||
|
||||
@@ -43,6 +43,7 @@ export const hasAccessLevel = (
|
||||
|
||||
/**
|
||||
* checks the current user is a guest
|
||||
* Company guests can be users accessing with a public link
|
||||
*
|
||||
* @param {CompanyExecutionContext} context
|
||||
* @returns {Promise<boolean>}
|
||||
@@ -149,7 +150,7 @@ export const getAccessLevel = async (
|
||||
context: CompanyExecutionContext & { public_token?: string; tdrive_tab_token?: string },
|
||||
): Promise<DriveFileAccessLevel | "none"> => {
|
||||
if (!id || id === "root")
|
||||
return !context?.user?.id ? "none" : (await isCompanyGuest(context)) ? "read" : "manage";
|
||||
return !context?.user?.id || (await isCompanyGuest(context)) ? "none" : "manage";
|
||||
if (id === "trash")
|
||||
return (await isCompanyGuest(context)) || !context?.user?.id
|
||||
? "none"
|
||||
@@ -164,6 +165,7 @@ export const getAccessLevel = async (
|
||||
}
|
||||
|
||||
let publicToken = context.public_token;
|
||||
const prevalidatedPublicTokenDocumentId = context?.user?.public_token_document_id;
|
||||
|
||||
try {
|
||||
item =
|
||||
@@ -187,10 +189,13 @@ export const getAccessLevel = async (
|
||||
*/
|
||||
|
||||
//Public access
|
||||
if (publicToken) {
|
||||
if (publicToken || prevalidatedPublicTokenDocumentId === item.id) {
|
||||
if (!item.access_info.public.token) return "none";
|
||||
const { token: itemToken, level: itemLevel, password, expiration } = item.access_info.public;
|
||||
if (expiration && expiration < Date.now()) return "none";
|
||||
if (prevalidatedPublicTokenDocumentId) {
|
||||
return itemLevel;
|
||||
}
|
||||
if (password) {
|
||||
const data = publicToken.split("+");
|
||||
if (data.length !== 2) return "none";
|
||||
@@ -227,7 +232,7 @@ export const getAccessLevel = async (
|
||||
const matchingCompany = accessEntities.find(
|
||||
a => a.type === "company" && a.id === context.company.id,
|
||||
);
|
||||
if (matchingCompany) otherLevels.push(matchingCompany.level);
|
||||
if (matchingCompany && !isCompanyGuest(context)) otherLevels.push(matchingCompany.level);
|
||||
}
|
||||
|
||||
//Parent folder
|
||||
|
||||
@@ -70,7 +70,7 @@ export class DocumentsProcessor
|
||||
content_keywords = extractKeywords(content_strings);
|
||||
} catch (error) {
|
||||
console.debug(error);
|
||||
logger.error("Failed to generate content keywords", error);
|
||||
logger.error({ error: `${error}` }, "Failed to generate content keywords");
|
||||
}
|
||||
|
||||
return { content_keywords, item: message.item };
|
||||
|
||||
@@ -81,7 +81,7 @@ export class DocumentsService {
|
||||
DriveTdriveTabEntity,
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error("Error while initializing Documents Service", error);
|
||||
logger.error({ error: `${error}` }, "Error while initializing Documents Service");
|
||||
}
|
||||
|
||||
return this;
|
||||
@@ -94,7 +94,10 @@ export class DocumentsService {
|
||||
* @param {DriveExecutionContext} context
|
||||
* @returns {Promise<DriveItemDetails>}
|
||||
*/
|
||||
get = async (id: string, context: DriveExecutionContext): Promise<DriveItemDetails> => {
|
||||
get = async (
|
||||
id: string,
|
||||
context: DriveExecutionContext & { public_token?: string },
|
||||
): Promise<DriveItemDetails> => {
|
||||
if (!context) {
|
||||
this.logger.error("invalid context");
|
||||
return null;
|
||||
@@ -123,11 +126,12 @@ export class DocumentsService {
|
||||
try {
|
||||
const hasAccess = await checkAccess(id, entity, "read", this.repository, context);
|
||||
if (!hasAccess) {
|
||||
this.logger.error("user does not have access drive item ", id);
|
||||
this.logger.error("user does not have access drive item " + id);
|
||||
throw Error("user does not have access to this item");
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error("Failed to grant access to the drive item", error);
|
||||
console.log(error);
|
||||
this.logger.error({ error: `${error}` }, "Failed to grant access to the drive item");
|
||||
throw new CrudException("User does not have access to this item or its children", 401);
|
||||
}
|
||||
|
||||
@@ -309,7 +313,7 @@ export class DocumentsService {
|
||||
|
||||
return driveItem;
|
||||
} catch (error) {
|
||||
this.logger.error("Failed to create drive item", error);
|
||||
this.logger.error({ error: `${error}` }, "Failed to create drive item");
|
||||
CrudException.throwMe(error, new CrudException("Failed to create item", 500));
|
||||
}
|
||||
};
|
||||
@@ -423,7 +427,7 @@ export class DocumentsService {
|
||||
return item;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
this.logger.error("Failed to update drive item", error);
|
||||
this.logger.error({ error: `${error}` }, "Failed to update drive item");
|
||||
throw new CrudException("Failed to update item", 500);
|
||||
}
|
||||
};
|
||||
@@ -468,7 +472,7 @@ export class DocumentsService {
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error("Failed to empty trash", error);
|
||||
this.logger.error({ error: `${error}` }, "Failed to empty trash");
|
||||
throw new CrudException("Failed to empty trash", 500);
|
||||
}
|
||||
|
||||
@@ -492,7 +496,7 @@ export class DocumentsService {
|
||||
throw Error("user does not have access to this item");
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error("Failed to grant access to the drive item", error);
|
||||
this.logger.error({ error: `${error}` }, "Failed to grant access to the drive item");
|
||||
throw new CrudException("User does not have access to this item or its children", 401);
|
||||
}
|
||||
|
||||
@@ -626,7 +630,7 @@ export class DocumentsService {
|
||||
|
||||
return driveItemVersion;
|
||||
} catch (error) {
|
||||
this.logger.error("Failed to create Drive item version", error);
|
||||
this.logger.error({ error: `${error}` }, "Failed to create Drive item version");
|
||||
throw new CrudException("Failed to create Drive item version", 500);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { ExecutionContext } from "../../core/platform/framework/api/crud-service";
|
||||
import { DriveFile } from "./entities/drive-file";
|
||||
import { FileVersion } from "./entities/file-version";
|
||||
import { SortType } from "src/core/platform/services/search/api";
|
||||
import { SortType } from "../../core/platform/services/search/api";
|
||||
|
||||
export interface CompanyExecutionContext extends ExecutionContext {
|
||||
company: { id: string };
|
||||
}
|
||||
|
||||
export type DriveExecutionContext = CompanyExecutionContext & {
|
||||
public_token?: string;
|
||||
};
|
||||
export type DriveExecutionContext = CompanyExecutionContext;
|
||||
|
||||
export type RequestParams = {
|
||||
company_id: string;
|
||||
|
||||
@@ -228,7 +228,7 @@ export const getPath = async (
|
||||
): Promise<DriveFile[]> => {
|
||||
id = id || "root";
|
||||
if (isVirtualFolder(id))
|
||||
return !context.public_token || ignoreAccess
|
||||
return !context?.user?.public_token_document_id || ignoreAccess
|
||||
? [
|
||||
{
|
||||
id,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { FastifyReply, FastifyRequest } from "fastify";
|
||||
import { getInstance } from "../../../../services/user/entities/user";
|
||||
import { logger } from "../../../../core/platform/framework";
|
||||
import { CrudException, ListResult } from "../../../../core/platform/framework/api/crud-service";
|
||||
import { File } from "../../../../services/files/entities/file";
|
||||
@@ -68,7 +69,7 @@ export class DocumentsController {
|
||||
context,
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error("Failed to create Drive item", error);
|
||||
logger.error({ error: `${error}` }, "Failed to create Drive item");
|
||||
CrudException.throwMe(error, new CrudException("Failed to create Drive item", 500));
|
||||
}
|
||||
};
|
||||
@@ -91,7 +92,7 @@ export class DocumentsController {
|
||||
|
||||
reply.status(200).send();
|
||||
} catch (error) {
|
||||
logger.error("Failed to delete drive item", error);
|
||||
logger.error({ error: `${error}` }, "Failed to delete drive item");
|
||||
throw new CrudException("Failed to delete drive item", 500);
|
||||
}
|
||||
};
|
||||
@@ -281,7 +282,7 @@ export class DocumentsController {
|
||||
response.send(data.file);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("failed to download file", error);
|
||||
logger.error({ error: `${error}` }, "failed to download file");
|
||||
throw new CrudException("Failed to download file", 500);
|
||||
}
|
||||
};
|
||||
@@ -328,7 +329,7 @@ export class DocumentsController {
|
||||
|
||||
archive.pipe(reply.raw);
|
||||
} catch (error) {
|
||||
logger.error("failed to send zip file", error);
|
||||
logger.error({ error: `${error}` }, "failed to send zip file");
|
||||
throw new CrudException("Failed to create zip file", 500);
|
||||
}
|
||||
};
|
||||
@@ -361,7 +362,7 @@ export class DocumentsController {
|
||||
|
||||
return this.driveFileDTOBuilder.build(fileList, context, options.fields, options.view);
|
||||
} catch (error) {
|
||||
logger.error("error while searching for document", error);
|
||||
logger.error({ error: `${error}` }, "error while searching for document");
|
||||
this.throw500Search();
|
||||
}
|
||||
};
|
||||
@@ -401,6 +402,65 @@ export class DocumentsController {
|
||||
context,
|
||||
);
|
||||
};
|
||||
|
||||
async getAnonymousToken(
|
||||
req: FastifyRequest<{
|
||||
Body: {
|
||||
company_id: string;
|
||||
document_id: string;
|
||||
token: string;
|
||||
token_password?: string;
|
||||
};
|
||||
}>,
|
||||
): Promise<{
|
||||
access_token: {
|
||||
time: number;
|
||||
expiration: number;
|
||||
refresh_expiration: number;
|
||||
value: string;
|
||||
refresh: string;
|
||||
type: string;
|
||||
};
|
||||
}> {
|
||||
const document = await globalResolver.services.documents.documents.get(req.body.document_id, {
|
||||
public_token: req.body.token + (req.body.token_password ? "+" + req.body.token_password : ""),
|
||||
user: null,
|
||||
company: { id: req.body.company_id },
|
||||
});
|
||||
|
||||
if (!document || !document.access || document.access === "none")
|
||||
throw new CrudException("You don't have access to this document", 401);
|
||||
|
||||
const email = document.item.company_id + "-anonymous@tdrive.com";
|
||||
let user = await globalResolver.services.users.getByEmail(email);
|
||||
if (!user) {
|
||||
user = (
|
||||
await globalResolver.services.users.create(
|
||||
getInstance({
|
||||
first_name: "Anonymous",
|
||||
last_name: "",
|
||||
email_canonical: email,
|
||||
username_canonical: (email.replace("@", ".") || "").toLocaleLowerCase(),
|
||||
phone: "",
|
||||
identity_provider: "anonymous",
|
||||
identity_provider_id: email,
|
||||
mail_verified: true,
|
||||
}),
|
||||
)
|
||||
).entity;
|
||||
}
|
||||
await globalResolver.services.companies.setUserRole(document.item.company_id, user.id, "guest");
|
||||
|
||||
const token = globalResolver.platformServices.auth.generateJWT(user.id, user.email_canonical, {
|
||||
track: false,
|
||||
provider_id: "tdrive",
|
||||
public_token_document_id: req.body.document_id,
|
||||
});
|
||||
|
||||
return {
|
||||
access_token: token,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -410,9 +470,8 @@ export class DocumentsController {
|
||||
* @returns {CompanyExecutionContext}
|
||||
*/
|
||||
const getDriveExecutionContext = (
|
||||
req: FastifyRequest<{ Params: { company_id: string }; Querystring?: { public_token?: string } }>,
|
||||
req: FastifyRequest<{ Params: { company_id: string } }>,
|
||||
): DriveExecutionContext => ({
|
||||
public_token: req.query?.public_token,
|
||||
user: req.currentUser,
|
||||
company: { id: req.params.company_id },
|
||||
url: req.url,
|
||||
|
||||
@@ -101,6 +101,12 @@ const routes: FastifyPluginCallback = (fastify: FastifyInstance, _options, next)
|
||||
handler: documentsController.setTab.bind(documentsController),
|
||||
});
|
||||
|
||||
fastify.route({
|
||||
method: "POST",
|
||||
url: `${baseUrl}/anonymous/token`,
|
||||
handler: documentsController.getAnonymousToken.bind(documentsController),
|
||||
});
|
||||
|
||||
return next();
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TdriveServiceProvider, Initializable } from "src/core/platform/framework/api";
|
||||
import Repository from "src/core/platform/services/database/services/orm/repository/repository";
|
||||
import { TdriveServiceProvider, Initializable } from "../../../core/platform/framework/api";
|
||||
import Repository from "../../../core/platform/services/database/services/orm/repository/repository";
|
||||
import { Tag, TagsType, TagsPrimaryKey } from "../entities";
|
||||
import gr from "../../global-resolver";
|
||||
import {
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from "../../../../utils/types";
|
||||
import { Tag } from "../../entities";
|
||||
import gr from "../../../global-resolver";
|
||||
import { ExecutionContext } from "src/core/platform/framework/api/crud-service";
|
||||
import { ExecutionContext } from "../../../../core/platform/framework/api/crud-service";
|
||||
import { handleError } from "../../../../utils/handleError";
|
||||
|
||||
export class TagsController
|
||||
|
||||
@@ -3,7 +3,7 @@ import { checkUserBelongsToCompany } from "../../../utils/company";
|
||||
import { Tag } from "../types";
|
||||
import { TagsController } from "./controllers";
|
||||
import gr from "../../global-resolver";
|
||||
import CompanyUser from "src/services/user/entities/company_user";
|
||||
import CompanyUser from "../../../services/user/entities/company_user";
|
||||
|
||||
const tagsUrl = "/companies/:company_id/tags";
|
||||
|
||||
|
||||
@@ -28,6 +28,8 @@ export interface User {
|
||||
application_id?: string;
|
||||
// allow_tracking
|
||||
allow_tracking?: boolean;
|
||||
// Prevalidated public token access
|
||||
public_token_document_id?: string;
|
||||
}
|
||||
|
||||
export const webSocketSchema = {
|
||||
|
||||
Reference in New Issue
Block a user