Procházet zdrojové kódy

Impl validation by comparing growi info

Taichi Masuyama před 3 roky
rodič
revize
f8444a9c69

+ 2 - 1
packages/app/src/interfaces/transfer-key.ts

@@ -1,5 +1,6 @@
 export interface ITransferKey<ID = string> {
 export interface ITransferKey<ID = string> {
   _id: ID
   _id: ID
   expireAt: Date
   expireAt: Date
-  value: string,
+  keyString: string,
+  key: string,
 }
 }

+ 10 - 13
packages/app/src/server/models/transfer-key.ts

@@ -1,6 +1,7 @@
 import { Model, Schema, HydratedDocument } from 'mongoose';
 import { Model, Schema, HydratedDocument } from 'mongoose';
 
 
 import { ITransferKey } from '~/interfaces/transfer-key';
 import { ITransferKey } from '~/interfaces/transfer-key';
+import { TransferKey } from '~/utils/vo/transfer-key';
 
 
 import loggerFactory from '../../utils/logger';
 import loggerFactory from '../../utils/logger';
 import { getOrCreateModel } from '../util/mongoose-utils';
 import { getOrCreateModel } from '../util/mongoose-utils';
@@ -8,14 +9,14 @@ import { getOrCreateModel } from '../util/mongoose-utils';
 const logger = loggerFactory('growi:models:transfer-key');
 const logger = loggerFactory('growi:models:transfer-key');
 
 
 interface ITransferKeyMethods {
 interface ITransferKeyMethods {
-  findOneActiveTransferKey(transferKeyString: string): Promise<HydratedDocument<ITransferKey, ITransferKeyMethods> | null>;
+  findOneActiveTransferKey(key: string): Promise<HydratedDocument<ITransferKey, ITransferKeyMethods> | null>;
 }
 }
 
 
 type TransferKeyModel = Model<ITransferKey, any, ITransferKeyMethods>;
 type TransferKeyModel = Model<ITransferKey, any, ITransferKeyMethods>;
 
 
 const schema = new Schema<ITransferKey, TransferKeyModel, ITransferKeyMethods>({
 const schema = new Schema<ITransferKey, TransferKeyModel, ITransferKeyMethods>({
   expireAt: { type: Date, default: () => new Date(), expires: '30m' },
   expireAt: { type: Date, default: () => new Date(), expires: '30m' },
-  value: { type: String, unique: true },
+  keyString: { type: String, unique: true }, // original key string
 }, {
 }, {
   timestamps: {
   timestamps: {
     createdAt: true,
     createdAt: true,
@@ -23,17 +24,13 @@ const schema = new Schema<ITransferKey, TransferKeyModel, ITransferKeyMethods>({
   },
   },
 });
 });
 
 
-schema.statics.findOneActiveTransferKey = async function(transferKeyString: string): Promise<HydratedDocument<ITransferKey, ITransferKeyMethods> | null> {
-  let tk: HydratedDocument<ITransferKey, ITransferKeyMethods> | null;
-  try {
-    tk = await this.findOne({ value: transferKeyString });
-  }
-  catch (err) {
-    logger.error(err);
-    throw err;
-  }
-
-  return tk;
+// Virtuals
+schema.virtual('key').get(function() {
+  return TransferKey.parse(this.keyString).key;
+});
+
+schema.statics.findOneActiveTransferKey = async function(key: string): Promise<HydratedDocument<ITransferKey, ITransferKeyMethods> | null> {
+  return this.findOne({ key });
 };
 };
 
 
 export default getOrCreateModel('TransferKey', schema);
 export default getOrCreateModel('TransferKey', schema);

+ 56 - 75
packages/app/src/server/routes/apiv3/g2g-transfer.ts

@@ -98,15 +98,15 @@ module.exports = (crowi: Crowi): Router => {
 
 
   // Local middleware to check if key is valid or not
   // Local middleware to check if key is valid or not
   const verifyAndExtractTransferKey = async(req: Request & { transferKey: TransferKey }, res: ApiV3Response, next: NextFunction) => {
   const verifyAndExtractTransferKey = async(req: Request & { transferKey: TransferKey }, res: ApiV3Response, next: NextFunction) => {
-    const transferKeyString = req.headers[X_GROWI_TRANSFER_KEY_HEADER_NAME];
+    const key = req.headers[X_GROWI_TRANSFER_KEY_HEADER_NAME];
 
 
-    if (typeof transferKeyString !== 'string') {
+    if (typeof key !== 'string') {
       return res.apiv3Err(new ErrorV3('Invalid transfer key or not set.', 'invalid_transfer_key'), 400);
       return res.apiv3Err(new ErrorV3('Invalid transfer key or not set.', 'invalid_transfer_key'), 400);
     }
     }
 
 
     let transferKey;
     let transferKey;
     try {
     try {
-      transferKey = await (TransferKeyModel as any).findOneActiveTransferKey(transferKeyString); // TODO: Improve TS of models
+      transferKey = await (TransferKeyModel as any).findOneActiveTransferKey(key); // TODO: Improve TS of models
     }
     }
     catch (err) {
     catch (err) {
       logger.error(err);
       logger.error(err);
@@ -119,7 +119,7 @@ module.exports = (crowi: Crowi): Router => {
 
 
     // Inject transferKey to req
     // Inject transferKey to req
     try {
     try {
-      req.transferKey = TransferKey.parse(transferKey.value);
+      req.transferKey = TransferKey.parse(transferKey.keyString);
     }
     }
     catch (err) {
     catch (err) {
       logger.error(err);
       logger.error(err);
@@ -135,14 +135,14 @@ module.exports = (crowi: Crowi): Router => {
 
 
   // Auto import
   // Auto import
   // eslint-disable-next-line max-len
   // eslint-disable-next-line max-len
-  receiveRouter.post('/', uploads.single('transferDataZipFile'), /* verifyAndExtractTransferKey, */ async(req: Request & { transferKey: TransferKey, operatorUserId: string }, res: ApiV3Response) => {
+  receiveRouter.post('/', uploads.single('transferDataZipFile'), verifyAndExtractTransferKey, async(req: Request & { transferKey: TransferKey, operatorUserId: string }, res: ApiV3Response) => {
     const { file } = req;
     const { file } = req;
 
 
     const zipFile = importService.getFile(file.filename);
     const zipFile = importService.getFile(file.filename);
-    let data;
 
 
     const { collections: strCollections, optionsMap: strOptionsMap, operatorUserId } = req.body;
     const { collections: strCollections, optionsMap: strOptionsMap, operatorUserId } = req.body;
 
 
+    // Parse multipart form data
     let collections;
     let collections;
     let optionsMap;
     let optionsMap;
     try {
     try {
@@ -170,88 +170,69 @@ module.exports = (crowi: Crowi): Router => {
     }
     }
     catch (err) {
     catch (err) {
       logger.error(err);
       logger.error(err);
-      // adminEvent.emit('onErrorForImport', { message: err.message });
-      return;
+      return res.apiv3Err(new ErrorV3('Failed to validate transfer data file.', 'validation_failed'), 500);
     }
     }
 
 
-    /*
-     * validate with meta.json
-     */
     try {
     try {
+      // validate with meta.json
       importService.validate(meta);
       importService.validate(meta);
     }
     }
     catch (err) {
     catch (err) {
       logger.error(err);
       logger.error(err);
-      // adminEvent.emit('onErrorForImport', { message: err.message });
-      return;
+
+      const msg = 'the version of this growi and the growi that exported the data are not met';
+      const varidationErr = 'version_incompatible';
+      return res.apiv3Err(new ErrorV3(msg, varidationErr), 500);
     }
     }
 
 
     // generate maps of ImportSettings to import
     // generate maps of ImportSettings to import
     const importSettingsMap = {};
     const importSettingsMap = {};
-    innerFileStats.forEach(({ fileName, collectionName }) => {
-      // instanciate GrowiArchiveImportOption
-      const options = new GrowiArchiveImportOption(null, optionsMap[collectionName]);
-
-      let importSettings;
-      // generate options
-      if (collectionName === 'configs') {
-        importSettings = importService.generateImportSettings('flushAndInsert');
-      }
-      else {
-        importSettings = importService.generateImportSettings('upsert');
-      }
-      importSettings.jsonFileName = fileName;
-
-      // generate overwrite params
-      importSettings.overwriteParams = generateOverwriteParams(collectionName, operatorUserId, options);
+    try {
+      innerFileStats.forEach(({ fileName, collectionName }) => {
+        // instanciate GrowiArchiveImportOption
+        const options = new GrowiArchiveImportOption(null, optionsMap[collectionName]);
 
 
-      importSettingsMap[collectionName] = importSettings;
-    });
+        let importSettings;
+        // generate options
+        if (collectionName === 'configs' && options.mode !== 'flushAndInsert') {
+          throw Error('`flushAndInsert` is only available as an import setting for configs collection');
+        }
+        if (collectionName === 'pages' && options.mode === 'insert') {
+          throw Error('`insert` is not available as an import setting for pages collection');
+        }
 
 
-    /*
-     * import
-     */
-    try {
-      importService.import(collections, importSettingsMap);
-      // const parameters = { action: SupportedAction.ACTION_ADMIN_GROWI_DATA_IMPORTED };
-      // activityEvent.emit('update', res.locals.activity._id, parameters);
-    }
-    catch (err) {
-      logger.error(err);
-      // adminEvent.emit('onErrorForImport', { message: err.message });
-    }
+        importSettings.jsonFileName = fileName;
 
 
-    // -----
+        // generate overwrite params
+        importSettings.overwriteParams = generateOverwriteParams(collectionName, operatorUserId, options);
 
 
-    try {
-      data = await growiBridgeService.parseZipFile(zipFile);
+        importSettingsMap[collectionName] = importSettings;
+      });
     }
     }
     catch (err) {
     catch (err) {
       logger.error(err);
       logger.error(err);
-      return res.apiv3Err(new ErrorV3('Failed to validate transfer data file.', 'validation_failed'), 500);
-    }
-
-    try {
-      // validate with meta.json
-      importService.validate(data.meta);
-
-      // const parameters = { action: SupportedAction.ACTION_ADMIN_ARCHIVE_DATA_UPLOAD };
-      // activityEvent.emit('update', res.locals.activity._id, parameters);
-    }
-    catch {
-      const msg = 'the version of this growi and the growi that exported the data are not met';
-      const varidationErr = 'versions-are-not-met';
-      return res.apiv3Err(new ErrorV3(msg, varidationErr), 500);
+      return res.apiv3Err(new ErrorV3('Import settings invalid. See growi docs about details.', 'import_settings_invalid'));
     }
     }
 
 
+    /*
+     * import
+     */
     try {
     try {
-      await g2gTransferReceiverService.receive(file.stream);
+      importService.import(collections, importSettingsMap);
     }
     }
     catch (err) {
     catch (err) {
       logger.error(err);
       logger.error(err);
-      return res.apiv3Err(new ErrorV3('Error occurred while importing transfer data.', 'failed_to_receive'));
+      return;
     }
     }
 
 
+    // try {
+    //   await g2gTransferReceiverService.receive(file.stream);
+    // }
+    // catch (err) {
+    //   logger.error(err);
+    //   return res.apiv3Err(new ErrorV3('Error occurred while importing transfer data.', 'failed_to_receive'));
+    // }
+
     return res.apiv3({ message: 'Successfully started to receive transfer data.' });
     return res.apiv3({ message: 'Successfully started to receive transfer data.' });
   });
   });
 
 
@@ -318,21 +299,21 @@ module.exports = (crowi: Crowi): Router => {
 
 
     // Ask growi info
     // Ask growi info
     // TODO: Ask progress as well
     // TODO: Ask progress as well
-    // let toGROWIInfo: IDataGROWIInfo;
-    // try {
-    //   toGROWIInfo = await g2gTransferPusherService.askGROWIInfo(tk);
-    // }
-    // catch (err) {
-    //   logger.error(err);
-    //   return res.apiv3Err(new ErrorV3('Error occurred while asking GROWI growi info.', 'failed_to_ask_growi_info'));
-    // }
+    let toGROWIInfo: IDataGROWIInfo;
+    try {
+      toGROWIInfo = await g2gTransferPusherService.askGROWIInfo(tk);
+    }
+    catch (err) {
+      logger.error(err);
+      return res.apiv3Err(new ErrorV3('Error occurred while asking GROWI growi info.', 'failed_to_ask_growi_info'));
+    }
 
 
     // Check if can transfer
     // Check if can transfer
-    // const canTransfer = await g2gTransferPusherService.canTransfer(toGROWIInfo);
-    // if (!canTransfer) {
-    //   logger.debug('Could not transfer.');
-    //   return res.apiv3Err(new ErrorV3('GROWI is incompatible to transfer data.', 'growi_incompatible_to_transfer'));
-    // }
+    const canTransfer = await g2gTransferPusherService.canTransfer(toGROWIInfo);
+    if (!canTransfer) {
+      logger.debug('Could not transfer.');
+      return res.apiv3Err(new ErrorV3('GROWI is incompatible to transfer data.', 'growi_incompatible_to_transfer'));
+    }
 
 
     // Start transfer
     // Start transfer
     try {
     try {

+ 15 - 4
packages/app/src/server/service/g2g-transfer.ts

@@ -63,6 +63,7 @@ interface Receiver {
    */
    */
   answerGROWIInfo(): Promise<IDataGROWIInfo>
   answerGROWIInfo(): Promise<IDataGROWIInfo>
   /**
   /**
+   * DO NOT USE TransferKeyModel.create() directly, instead, use this method to create a TransferKey document.
    * This method receives appSiteUrl to create a TransferKey document and returns generated transfer key string.
    * This method receives appSiteUrl to create a TransferKey document and returns generated transfer key string.
    * UUID is the same value as the created document's _id.
    * UUID is the same value as the created document's _id.
    * @param {URL} appSiteUrl URL type appSiteUrl
    * @param {URL} appSiteUrl URL type appSiteUrl
@@ -106,9 +107,19 @@ export class G2GTransferPusherService implements Pusher {
   }
   }
 
 
   public async canTransfer(toGROWIInfo: IDataGROWIInfo): Promise<boolean> {
   public async canTransfer(toGROWIInfo: IDataGROWIInfo): Promise<boolean> {
-    // Compare GROWIInfos
+    const configManager = this.crowi.configManager;
+    const userUpperLimit = configManager.getConfig('crowi', 'security:userUpperLimit');
+    const version = this.crowi.version;
+
+    if (version !== toGROWIInfo.version) {
+      return false;
+    }
+
+    if (userUpperLimit < (toGROWIInfo.userUpperLimit ?? Infinity)) {
+      return false;
+    }
 
 
-    return false;
+    return true;
   }
   }
 
 
   public async transferAttachments(): Promise<void> { return }
   public async transferAttachments(): Promise<void> { return }
@@ -232,14 +243,14 @@ export class G2GTransferReceiverService implements Receiver {
     // Save TransferKey document
     // Save TransferKey document
     let tkd;
     let tkd;
     try {
     try {
-      tkd = await TransferKeyModel.create({ _id: uuid, value: transferKeyString });
+      tkd = await TransferKeyModel.create({ _id: uuid, keyString: transferKeyString });
     }
     }
     catch (err) {
     catch (err) {
       logger.error(err);
       logger.error(err);
       throw err;
       throw err;
     }
     }
 
 
-    return tkd.value;
+    return tkd.keyString;
   }
   }
 
 
   public async receive(zipfile: Readable): Promise<void> {
   public async receive(zipfile: Readable): Promise<void> {

+ 4 - 0
packages/app/src/utils/vo/transfer-key.ts

@@ -14,6 +14,10 @@ export class TransferKey {
     this.key = key;
     this.key = key;
   }
   }
 
 
+  get getKeyString(): string {
+    return TransferKey.generateKeyString(this.key, this.appUrl);
+  }
+
   /**
   /**
    * Parse a transfer key string generated by the generateKeyString static method
    * Parse a transfer key string generated by the generateKeyString static method
    * @param {string} keyString Transfer key string
    * @param {string} keyString Transfer key string