Skip to content

Commit

Permalink
Merge pull request #242 from ardriveapp/PE-6440-files-in-private-driv…
Browse files Browse the repository at this point in the history
…e-appearing-as-public-when-uploaded-via-cli

PE-6440: fix(uploads): verify drive privacy
  • Loading branch information
thiagocarvalhodev authored Jul 22, 2024
2 parents 8907470 + 7b6ec3d commit 346f25d
Show file tree
Hide file tree
Showing 7 changed files with 75 additions and 8 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ardrive-core-js",
"version": "2.0.3",
"version": "2.0.5",
"description": "ArDrive Core contains the essential back end application features to support the ArDrive CLI and Desktop apps, such as file management, Permaweb upload/download, wallet management and other common functions.",
"main": "./lib/exports.js",
"types": "./lib/exports.d.ts",
Expand Down
14 changes: 13 additions & 1 deletion src/ardrive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -598,8 +598,11 @@ export class ArDrive extends ArDriveAnonymous {
for (const entity of entitiesToUpload) {
const { destFolderId } = entity;
const destDriveId = await this.arFsDao.getDriveIdForFolderId(destFolderId);

const owner = await this.wallet.getAddress();

// Assert that the drive has the correct privacy settings
await this.arFsDao.assertDrivePrivacy(destDriveId, owner, entity.driveKey);

preparedEntities.push({ ...entity, destDriveId, owner });
}

Expand Down Expand Up @@ -1035,6 +1038,9 @@ export class ArDrive extends ArDriveAnonymous {
const driveId = await this.arFsDao.getDriveIdForFolderId(parentFolderId);
const owner = await this.wallet.getAddress();

// Assert that the drive is public
await this.arFsDao.assertDrivePrivacy(driveId, owner);

// Assert that there are no duplicate names in the destination folder
const entityNamesInParentFolder = await this.arFsDao.getPublicEntityNamesInFolder(
parentFolderId,
Expand Down Expand Up @@ -1100,6 +1106,9 @@ export class ArDrive extends ArDriveAnonymous {
const driveId = await this.arFsDao.getDriveIdForFolderId(parentFolderId);
const owner = await this.wallet.getAddress();

// Assert that the drive is private
await this.arFsDao.assertDrivePrivacy(driveId, owner, driveKey);

// Assert that there are no duplicate names in the destination folder
const entityNamesInParentFolder = await this.arFsDao.getPrivateEntityNamesInFolder(
parentFolderId,
Expand Down Expand Up @@ -1558,6 +1567,7 @@ export class ArDrive extends ArDriveAnonymous {
const owner = await this.wallet.getAddress();
const file = await this.getPrivateFile({ fileId, driveKey, owner });
const driveId = await this.getDriveIdForFileId(fileId);

if (file.name === newName) {
throw new Error(`To rename a file, the new name must be different`);
}
Expand Down Expand Up @@ -1622,6 +1632,7 @@ export class ArDrive extends ArDriveAnonymous {
const owner = await this.wallet.getAddress();
const folder = await this.getPublicFolder({ folderId, owner });
const driveId = await this.getDriveIdForFolderId(folderId);

if (`${folder.parentFolderId}` === ROOT_FOLDER_ID_PLACEHOLDER) {
throw new Error(
`The root folder with ID '${folderId}' cannot be renamed as it shares its name with its parent drive. Consider renaming the drive instead.`
Expand Down Expand Up @@ -1743,6 +1754,7 @@ export class ArDrive extends ArDriveAnonymous {
async renamePublicDrive({ driveId, newName }: RenamePublicDriveParams): Promise<ArFSResult> {
const owner = await this.wallet.getAddress();
const drive = await this.getPublicDrive({ driveId, owner });

if (drive.name === newName) {
throw new Error(`New drive name '${newName}' must be different from the current drive name!`);
}
Expand Down
4 changes: 2 additions & 2 deletions src/arfs/arfs_builders/arfs_drive_builders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
EntityMetaDataTransactionData
} from '../../types';
import { Utf8ArrayToStr } from '../../utils/common';
import { ENCRYPTED_DATA_PLACEHOLDER, fakeEntityId } from '../../utils/constants';
import { ENCRYPTED_DATA_PLACEHOLDER, fakeEntityId, gqlTagNameRecord } from '../../utils/constants';
import { ArFSPublicDrive, ArFSPrivateDrive, ArFSDriveEntity } from '../arfs_entities';
import {
ArFSMetadataEntityBuilder,
Expand Down Expand Up @@ -171,7 +171,7 @@ export class ArFSPrivateDriveBuilder extends ArFSDriveBuilder<ArFSPrivateDrive>
case 'Drive-Auth-Mode':
this.driveAuthMode = value as DriveAuthMode;
break;
case 'Drive-Privacy':
case gqlTagNameRecord.drivePrivacy:
this.drivePrivacy = value as DrivePrivacy;
break;
default:
Expand Down
18 changes: 18 additions & 0 deletions src/arfs/arfsdao.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,8 @@ describe('The ArFSDAO class', () => {
it('returns the expected result for an upload plan with a private file as v2 transactions', async () => {
// Use an expected id so we can expect an exact file key
const fileWithExistingId = stubFileUploadStats();
stub(arfsDao, 'assertDrivePrivacy').resolves();

fileWithExistingId.wrappedEntity.existingId = stubEntityID;

const { fileResults, bundleResults, folderResults } = await arfsDao.uploadAllEntities({
Expand All @@ -601,6 +603,8 @@ describe('The ArFSDAO class', () => {
});

it('returns the expected result for an upload plan with a private folder as v2 transactions', async () => {
stub(arfsDao, 'assertDrivePrivacy').resolves();

const { fileResults, bundleResults, folderResults } = await arfsDao.uploadAllEntities({
bundlePlans: [],
v2TxPlans: {
Expand All @@ -622,6 +626,8 @@ describe('The ArFSDAO class', () => {
});

it('returns the expected result for an upload plan with a public folder that has an expected folder id sent as a v2 transaction', async () => {
stub(arfsDao, 'assertDrivePrivacy').resolves();

const folderWithExpectedId = stubFolderUploadStats();
folderWithExpectedId.wrappedEntity.existingId = stubEntityID;

Expand All @@ -646,6 +652,8 @@ describe('The ArFSDAO class', () => {
});

it('returns the expected result for an upload plan with a private folder that has an expected folder id sent as a v2 transaction', async () => {
stub(arfsDao, 'assertDrivePrivacy').resolves();

const folderWithExpectedId = stubFolderUploadStats();
folderWithExpectedId.wrappedEntity.existingId = stubEntityID;

Expand All @@ -670,6 +678,8 @@ describe('The ArFSDAO class', () => {
});

it('returns the expected result for an upload plan with a folder and a file as v2 transactions', async () => {
stub(arfsDao, 'assertDrivePrivacy').resolves();

const { fileResults, bundleResults, folderResults } = await arfsDao.uploadAllEntities({
bundlePlans: [],
v2TxPlans: {
Expand Down Expand Up @@ -700,6 +710,8 @@ describe('The ArFSDAO class', () => {
});

it('returns the expected result for an upload plan with a single file as a bundled transaction', async () => {
stub(arfsDao, 'assertDrivePrivacy').resolves();

const { fileResults, bundleResults, folderResults } = await arfsDao.uploadAllEntities({
bundlePlans: [
{
Expand All @@ -721,6 +733,8 @@ describe('The ArFSDAO class', () => {
});

it('returns the expected result for an upload plan with a two folders as a bundled transaction', async () => {
stub(arfsDao, 'assertDrivePrivacy').resolves();

const { fileResults, bundleResults, folderResults } = await arfsDao.uploadAllEntities({
bundlePlans: [
{
Expand All @@ -744,6 +758,8 @@ describe('The ArFSDAO class', () => {
});

it('returns the expected result for an upload plan with a folder and a file as a bundled transaction', async () => {
stub(arfsDao, 'assertDrivePrivacy').resolves();

const { fileResults, bundleResults, folderResults } = await arfsDao.uploadAllEntities({
bundlePlans: [
{
Expand All @@ -766,6 +782,8 @@ describe('The ArFSDAO class', () => {
});

it('returns the expected result for an upload plan with many files and folders sent as multiple bundled transactions', async () => {
stub(arfsDao, 'assertDrivePrivacy').resolves();

const { fileResults, bundleResults, folderResults } = await arfsDao.uploadAllEntities({
bundlePlans: [
{
Expand Down
39 changes: 36 additions & 3 deletions src/arfs/arfsdao.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ import {
authTagLength,
defaultMaxConcurrentChunks,
ENCRYPTED_DATA_PLACEHOLDER,
turboProdUrl
turboProdUrl,
gqlTagNameRecord
} from '../utils/constants';
import { PrivateKeyData } from './private_key_data';
import {
Expand Down Expand Up @@ -105,7 +106,7 @@ import {
fileConflictInfoMap,
folderToNameAndIdMap
} from '../utils/mapper_functions';
import { buildQuery, ASCENDING_ORDER } from '../utils/query';
import { buildQuery, ASCENDING_ORDER, DESCENDING_ORDER } from '../utils/query';
import { Wallet } from '../wallet';
import { JWKWallet } from '../jwk_wallet';
import { ArFSEntityCache } from './arfs_entity_cache';
Expand Down Expand Up @@ -175,7 +176,7 @@ import {
} from './tx/arfs_tx_data_types';
import { ArFSTagAssembler } from './tags/tag_assembler';
import { assertDataRootsMatch, rePrepareV2Tx } from '../utils/arfsdao_utils';
import { ArFSDataToUpload, ArFSFolderToUpload, DrivePrivacy } from '../exports';
import { ArFSDataToUpload, ArFSFolderToUpload, DrivePrivacy, errorMessage } from '../exports';
import { Turbo, TurboCachesResponse } from './turbo';
import { ArweaveSigner } from 'arbundles/src/signing';

Expand Down Expand Up @@ -1777,6 +1778,38 @@ export class ArFSDAO extends ArFSDAOAnonymous {
);
}

public async isPublicDrive(driveId: DriveID, address: ArweaveAddress): Promise<boolean> {
const gqlQuery = buildQuery({
tags: [
{ name: 'Entity-Type', value: 'drive' },
{ name: 'Drive-Id', value: `${driveId}` }
],
owner: address,
sort: DESCENDING_ORDER
});

const transactions = await this.gatewayApi.gqlRequest(gqlQuery);

const drivePrivacyFromTag = transactions.edges[0].node.tags.find(
(t) => t.name === gqlTagNameRecord.drivePrivacy
);

return drivePrivacyFromTag?.value === 'public';
}

public async assertDrivePrivacy(driveId: DriveID, address: ArweaveAddress, driveKey?: DriveKey): Promise<void> {
const _isPublicDrive = await this.isPublicDrive(driveId, address);

// Private drive uploads require a drive key
if (!_isPublicDrive && !driveKey) {
throw new Error(errorMessage.privateDriveRequiresDriveKey);
}

if (_isPublicDrive && driveKey) {
throw new Error(errorMessage.publicDriveDoesNotRequireDriveKey);
}
}

public async getOwnerAndAssertDrive(driveId: DriveID, driveKey?: DriveKey): Promise<ArweaveAddress> {
const cachedOwner = this.caches.ownerCache.get(driveId);
if (cachedOwner) {
Expand Down
4 changes: 3 additions & 1 deletion src/utils/error_message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,7 @@ export const errorMessage = {
folderCannotMoveIntoItself: 'Folders cannot be moved into themselves!',
fileIsTheSame: 'The file to upload matches an existing file entity!',
cannotMoveIntoSamePlace: (type: 'File' | 'Folder', parentFolderId: FolderID): string =>
`${type} already has parent folder with ID: ${parentFolderId}`
`${type} already has parent folder with ID: ${parentFolderId}`,
privateDriveRequiresDriveKey: 'Private drive requires a drive key to upload.',
publicDriveDoesNotRequireDriveKey: 'Public drive does not require a drive key to upload.'
};
2 changes: 2 additions & 0 deletions tests/integration/ardrive.int.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ describe('ArDrive class - integrated', () => {
);

const walletOwner = stubArweaveAddress();

// Use copies to expose any issues with object equality in tested code
const expectedDriveId = EID(stubEntityID.toString());
const unexpectedDriveId = EID(stubEntityIDAlt.toString());
Expand Down Expand Up @@ -220,6 +221,7 @@ describe('ArDrive class - integrated', () => {
stub(walletDao, 'walletHasBalance').resolves(true);
stub(wallet, 'getAddress').resolves(walletOwner);
stub(arfsDao, 'getDriveIDForEntityId').resolves(expectedDriveId);
stub(arfsDao, 'assertDrivePrivacy').resolves();
});

describe('utility function', () => {
Expand Down

0 comments on commit 346f25d

Please sign in to comment.