Skip to content

Commit

Permalink
Merge pull request #155 from ardriveapp/PE-679_create_folder_assert_p…
Browse files Browse the repository at this point in the history
…rivacy

fix(create folder): Assert drive privacy during folder creation PE-679
  • Loading branch information
fedellen authored Nov 12, 2021
2 parents 4f537ed + b453400 commit 9353fd1
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 29 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-cli",
"version": "1.0.3",
"version": "1.0.4",
"description": "The ArDrive Command Line Interface (CLI is a Node.js application for terminal-based ArDrive workflows. It also offers utility operations for securely interacting with Arweave wallets and inspecting various Arweave blockchain conditions.",
"main": "./lib/index.js",
"bin": {
Expand Down
12 changes: 6 additions & 6 deletions src/ardrive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -521,7 +521,7 @@ export class ArDrive extends ArDriveAnonymous {
}: UploadPublicFileParams): Promise<ArFSResult> {
const driveId = await this.arFsDao.getDriveIdForFolderId(parentFolderId);

const owner = await this.getOwnerForDriveId(driveId);
const owner = await this.arFsDao.getOwnerAndAssertDrive(driveId);
await this.assertOwnerAddress(owner);

// Derive destination name and names already within provided destination folder
Expand Down Expand Up @@ -610,7 +610,7 @@ export class ArDrive extends ArDriveAnonymous {
}: BulkPublicUploadParams): Promise<ArFSResult> {
const driveId = await this.arFsDao.getDriveIdForFolderId(parentFolderId);

const owner = await this.getOwnerForDriveId(driveId);
const owner = await this.arFsDao.getOwnerAndAssertDrive(driveId);
await this.assertOwnerAddress(owner);

// Derive destination name and names already within provided destination folder
Expand Down Expand Up @@ -817,7 +817,7 @@ export class ArDrive extends ArDriveAnonymous {
}: UploadPrivateFileParams): Promise<ArFSResult> {
const driveId = await this.arFsDao.getDriveIdForFolderId(parentFolderId);

const owner = await this.getOwnerForDriveId(driveId);
const owner = await this.arFsDao.getOwnerAndAssertDrive(driveId, driveKey);
await this.assertOwnerAddress(owner);

// Derive destination name and names already within provided destination folder
Expand Down Expand Up @@ -920,7 +920,7 @@ export class ArDrive extends ArDriveAnonymous {
const driveId = await this.arFsDao.getDriveIdForFolderId(parentFolderId);

// Get owner of drive, will error if no drives are found
const owner = await this.getOwnerForDriveId(driveId);
const owner = await this.arFsDao.getOwnerAndAssertDrive(driveId, driveKey);

// Assert that the provided wallet is the owner of the drive
await this.assertOwnerAddress(owner);
Expand Down Expand Up @@ -1134,7 +1134,7 @@ export class ArDrive extends ArDriveAnonymous {
}

async createPublicFolder({ folderName, driveId, parentFolderId }: CreatePublicFolderParams): Promise<ArFSResult> {
const owner = await this.getOwnerForDriveId(driveId);
const owner = await this.arFsDao.getOwnerAndAssertDrive(driveId);
await this.assertOwnerAddress(owner);

// Assert that there are no duplicate names in the destination folder
Expand Down Expand Up @@ -1179,7 +1179,7 @@ export class ArDrive extends ArDriveAnonymous {
driveKey,
parentFolderId
}: CreatePrivateFolderParams): Promise<ArFSResult> {
const owner = await this.getOwnerForDriveId(driveId);
const owner = await this.arFsDao.getOwnerAndAssertDrive(driveId, driveKey);
await this.assertOwnerAddress(owner);

// Assert that there are no duplicate names in the destination folder
Expand Down
61 changes: 60 additions & 1 deletion src/arfsdao.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,15 @@ import type { JWKWallet, Wallet } from './wallet';
import Arweave from 'arweave';
import { v4 as uuidv4 } from 'uuid';
import Transaction from 'arweave/node/lib/transaction';
import { deriveDriveKey, GQLEdgeInterface, GQLNodeInterface, GQLTagInterface, JWKInterface } from 'ardrive-core-js';
import {
deriveDriveKey,
GQLEdgeInterface,
GQLNodeInterface,
GQLTagInterface,
JWKInterface,
DrivePrivacy,
driveDecrypt
} from 'ardrive-core-js';
import {
ArFSPublicFileDataPrototype,
ArFSObjectMetadataPrototype,
Expand Down Expand Up @@ -908,6 +916,57 @@ export class ArFSDAO extends ArFSDAOAnonymous {
);
}

public async getOwnerAndAssertDrive(driveId: DriveID, driveKey?: DriveKey): Promise<ArweaveAddress> {
const gqlQuery = buildQuery({
tags: [
{ name: 'Entity-Type', value: 'drive' },
{ name: 'Drive-Id', value: `${driveId}` }
],
sort: ASCENDING_ORDER
});
const response = await this.arweave.api.post(graphQLURL, gqlQuery);
const edges: GQLEdgeInterface[] = response.data.data.transactions.edges;

if (!edges.length) {
throw new Error(`Could not find a transaction with "Drive-Id": ${driveId}`);
}

const edgeOfFirstDrive = edges[0];

const drivePrivacy: DrivePrivacy = driveKey ? 'private' : 'public';
const drivePrivacyFromTag = edgeOfFirstDrive.node.tags.find((t) => t.name === 'Drive-Privacy');

if (!drivePrivacyFromTag) {
throw new Error('Target drive has no "Drive-Privacy" tag!');
}

if (drivePrivacyFromTag.value !== drivePrivacy) {
throw new Error(`Target drive is not a ${drivePrivacy} drive!`);
}

if (driveKey) {
const cipherIVFromTag = edgeOfFirstDrive.node.tags.find((t) => t.name === 'Cipher-IV');
if (!cipherIVFromTag) {
throw new Error('Target private drive has no "Cipher-IV" tag!');
}

const driveDataBuffer = Buffer.from(
await this.arweave.transactions.getData(edgeOfFirstDrive.node.id, { decode: true })
);

try {
// Attempt to decrypt drive to assert drive key is correct
await driveDecrypt(cipherIVFromTag.value, driveKey, driveDataBuffer);
} catch {
throw new Error('Provided drive key or password could not decrypt target private drive!');
}
}

const driveOwnerAddress = edgeOfFirstDrive.node.owner.address;

return new ArweaveAddress(driveOwnerAddress);
}

/**
* Lists the children of certain private folder
* @param {FolderID} folderId the folder ID to list children of
Expand Down
9 changes: 8 additions & 1 deletion src/arfsdao_anonymous.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,13 @@ export class ArFSDAOAnonymous extends ArFSDAOType {
}

public async getOwnerForDriveId(driveId: DriveID): Promise<ArweaveAddress> {
const gqlQuery = buildQuery({ tags: [{ name: 'Drive-Id', value: driveId }], sort: ASCENDING_ORDER });
const gqlQuery = buildQuery({
tags: [
{ name: 'Drive-Id', value: `${driveId}` },
{ name: 'Entity-Type', value: 'drive' }
],
sort: ASCENDING_ORDER
});
const response = await this.arweave.api.post(graphQLURL, gqlQuery);
const edges: GQLEdgeInterface[] = response.data.data.transactions.edges;

Expand All @@ -55,6 +61,7 @@ export class ArFSDAOAnonymous extends ArFSDAOType {
}

const edgeOfFirstDrive = edges[0];

const driveOwnerAddress = edgeOfFirstDrive.node.owner.address;

return new ArweaveAddress(driveOwnerAddress);
Expand Down
40 changes: 20 additions & 20 deletions tests/integration/ardrive.int.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ describe('ArDrive class - integrated', () => {
});

it('throws an error if the owner of the drive conflicts with supplied wallet', async () => {
stub(arfsDao, 'getOwnerForDriveId').resolves(unexpectedOwner);
stub(arfsDao, 'getOwnerAndAssertDrive').resolves(unexpectedOwner);

await expectAsyncErrorThrow({
promiseToError: arDrive.createPublicFolder({
Expand All @@ -139,7 +139,7 @@ describe('ArDrive class - integrated', () => {
});

it('throws an error if the folder name conflicts with another ENTITY name in the destination folder', async () => {
stub(arfsDao, 'getOwnerForDriveId').resolves(walletOwner);
stub(arfsDao, 'getOwnerAndAssertDrive').resolves(walletOwner);

await expectAsyncErrorThrow({
promiseToError: arDrive.createPublicFolder({
Expand All @@ -152,7 +152,7 @@ describe('ArDrive class - integrated', () => {
});

it('returns the correct ArFSResult', async () => {
stub(arfsDao, 'getOwnerForDriveId').resolves(walletOwner);
stub(arfsDao, 'getOwnerAndAssertDrive').resolves(walletOwner);
stub(arfsDao, 'getPublicDrive').resolves(stubPublicDrive);

const result = await arDrive.createPublicFolder({
Expand All @@ -170,7 +170,7 @@ describe('ArDrive class - integrated', () => {
});

it('throws an error if the owner of the drive conflicts with supplied wallet', async () => {
stub(arfsDao, 'getOwnerForDriveId').resolves(unexpectedOwner);
stub(arfsDao, 'getOwnerAndAssertDrive').resolves(unexpectedOwner);

await expectAsyncErrorThrow({
promiseToError: arDrive.createPrivateFolder({
Expand All @@ -184,7 +184,7 @@ describe('ArDrive class - integrated', () => {
});

it('throws an error if the folder name conflicts with another ENTITY name in the destination folder', async () => {
stub(arfsDao, 'getOwnerForDriveId').resolves(walletOwner);
stub(arfsDao, 'getOwnerAndAssertDrive').resolves(walletOwner);

await expectAsyncErrorThrow({
promiseToError: arDrive.createPrivateFolder({
Expand All @@ -199,7 +199,7 @@ describe('ArDrive class - integrated', () => {

it('returns the correct ArFSResult', async () => {
stub(arfsDao, 'getPrivateDrive').resolves(stubPrivateDrive);
stub(arfsDao, 'getOwnerForDriveId').resolves(walletOwner);
stub(arfsDao, 'getOwnerAndAssertDrive').resolves(walletOwner);

const stubDriveKey = await getStubDriveKey();
const result = await arDrive.createPrivateFolder({
Expand Down Expand Up @@ -474,7 +474,7 @@ describe('ArDrive class - integrated', () => {
});

it('throws an error if the owner of the drive conflicts with supplied wallet', async () => {
stub(arfsDao, 'getOwnerForDriveId').resolves(unexpectedOwner);
stub(arfsDao, 'getOwnerAndAssertDrive').resolves(unexpectedOwner);

await expectAsyncErrorThrow({
promiseToError: arDrive.uploadPublicFile({ parentFolderId: stubEntityID, wrappedFile }),
Expand All @@ -483,7 +483,7 @@ describe('ArDrive class - integrated', () => {
});

it('throws an error if destination folder has a conflicting FOLDER name', async () => {
stub(arfsDao, 'getOwnerForDriveId').resolves(walletOwner);
stub(arfsDao, 'getOwnerAndAssertDrive').resolves(walletOwner);

await expectAsyncErrorThrow({
promiseToError: arDrive.uploadPublicFile({
Expand All @@ -496,7 +496,7 @@ describe('ArDrive class - integrated', () => {
});

it('returns the correct empty ArFSResult if destination folder has a conflicting FILE name and conflict resolution is set to skip', async () => {
stub(arfsDao, 'getOwnerForDriveId').resolves(walletOwner);
stub(arfsDao, 'getOwnerAndAssertDrive').resolves(walletOwner);

const result = await arDrive.uploadPublicFile({
parentFolderId: stubEntityID,
Expand All @@ -513,7 +513,7 @@ describe('ArDrive class - integrated', () => {
});

it('returns the correct ArFSResult revision if destination folder has a conflicting FILE name and conflict resolution is set to replace', async () => {
stub(arfsDao, 'getOwnerForDriveId').resolves(walletOwner);
stub(arfsDao, 'getOwnerAndAssertDrive').resolves(walletOwner);

const result = await arDrive.uploadPublicFile({
parentFolderId: stubEntityID,
Expand All @@ -527,7 +527,7 @@ describe('ArDrive class - integrated', () => {
});

it('returns an empty ArFSResult if destination folder has a conflicting FILE name and a matching last modified date and the conflict resolution is set to upsert', async () => {
stub(arfsDao, 'getOwnerForDriveId').resolves(walletOwner);
stub(arfsDao, 'getOwnerAndAssertDrive').resolves(walletOwner);
stub(wrappedFile, 'lastModifiedDate').get(() => matchingLastModifiedDate);

const result = await arDrive.uploadPublicFile({
Expand All @@ -545,7 +545,7 @@ describe('ArDrive class - integrated', () => {
});

it('returns the correct ArFSResult revision if destination folder has a conflicting FILE name and a different last modified date and the conflict resolution is set to upsert', async () => {
stub(arfsDao, 'getOwnerForDriveId').resolves(walletOwner);
stub(arfsDao, 'getOwnerAndAssertDrive').resolves(walletOwner);
stub(wrappedFile, 'lastModifiedDate').get(() => differentLastModifiedDate);

const result = await arDrive.uploadPublicFile({
Expand All @@ -560,7 +560,7 @@ describe('ArDrive class - integrated', () => {
});

it('returns the correct ArFSResult', async () => {
stub(arfsDao, 'getOwnerForDriveId').resolves(walletOwner);
stub(arfsDao, 'getOwnerAndAssertDrive').resolves(walletOwner);

const result = await arDrive.uploadPublicFile({ parentFolderId: stubEntityID, wrappedFile });
assertUploadFileExpectations(result, 3204, 166, 0, '1', 'public');
Expand All @@ -587,7 +587,7 @@ describe('ArDrive class - integrated', () => {
});

it('throws an error if the owner of the drive conflicts with supplied wallet', async () => {
stub(arfsDao, 'getOwnerForDriveId').resolves(unexpectedOwner);
stub(arfsDao, 'getOwnerAndAssertDrive').resolves(unexpectedOwner);

await expectAsyncErrorThrow({
promiseToError: arDrive.uploadPrivateFile({
Expand All @@ -600,7 +600,7 @@ describe('ArDrive class - integrated', () => {
});

it('throws an error if destination folder has a conflicting FOLDER name', async () => {
stub(arfsDao, 'getOwnerForDriveId').resolves(walletOwner);
stub(arfsDao, 'getOwnerAndAssertDrive').resolves(walletOwner);

await expectAsyncErrorThrow({
promiseToError: arDrive.uploadPrivateFile({
Expand All @@ -614,7 +614,7 @@ describe('ArDrive class - integrated', () => {
});

it('returns the correct empty ArFSResult if destination folder has a conflicting FILE name and conflict resolution is set to skip', async () => {
stub(arfsDao, 'getOwnerForDriveId').resolves(walletOwner);
stub(arfsDao, 'getOwnerAndAssertDrive').resolves(walletOwner);

const result = await arDrive.uploadPrivateFile({
parentFolderId: stubEntityID,
Expand All @@ -632,7 +632,7 @@ describe('ArDrive class - integrated', () => {
});

it('returns the correct ArFSResult revision with if destination folder has a conflicting FILE name and conflict resolution is set to replace', async () => {
stub(arfsDao, 'getOwnerForDriveId').resolves(walletOwner);
stub(arfsDao, 'getOwnerAndAssertDrive').resolves(walletOwner);

const result = await arDrive.uploadPrivateFile({
parentFolderId: stubEntityID,
Expand All @@ -647,7 +647,7 @@ describe('ArDrive class - integrated', () => {
});

it('returns an empty ArFSResult if destination folder has a conflicting FILE name and a matching last modified date and the conflict resolution is set to upsert', async () => {
stub(arfsDao, 'getOwnerForDriveId').resolves(walletOwner);
stub(arfsDao, 'getOwnerAndAssertDrive').resolves(walletOwner);
stub(wrappedFile, 'lastModifiedDate').get(() => matchingLastModifiedDate);

const result = await arDrive.uploadPrivateFile({
Expand All @@ -666,7 +666,7 @@ describe('ArDrive class - integrated', () => {
});

it('returns the correct ArFSResult revision if destination folder has a conflicting FILE name and a different last modified date and the conflict resolution is set to upsert', async () => {
stub(arfsDao, 'getOwnerForDriveId').resolves(walletOwner);
stub(arfsDao, 'getOwnerAndAssertDrive').resolves(walletOwner);
stub(wrappedFile, 'lastModifiedDate').get(() => differentLastModifiedDate);

const result = await arDrive.uploadPrivateFile({
Expand All @@ -682,7 +682,7 @@ describe('ArDrive class - integrated', () => {
});

it('returns the correct ArFSResult', async () => {
stub(arfsDao, 'getOwnerForDriveId').resolves(walletOwner);
stub(arfsDao, 'getOwnerAndAssertDrive').resolves(walletOwner);
const stubDriveKey = await getStubDriveKey();

const result = await arDrive.uploadPrivateFile({
Expand Down

0 comments on commit 9353fd1

Please sign in to comment.