Skip to content

Conversation

@bojeil-google
Copy link
Contributor

This API can be used for bulk account import to Firebase Auth user database from external Auth systems using different hashing algorithms as well as other Firebase projects.
Unit tests and integration tests also added for this new API.

fix(auth): Fixes quota exceeded errors thrown in auth integration tests due to backend throttling.

This API can be used for bulk account import to Firebase Auth user database
from external Auth systems using different hashing algorithms as well as
other Firebase projects.

fix(auth): Fixes quota exceeded errors thrown in auth integration tests due
to backend throttling.
Copy link
Contributor

@hiranya911 hiranya911 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code looks correct. Most of my comments are about style and readability.

src/auth/auth.ts Outdated
}

/**
* Imports the list of users provided to Firebase Auth users database. This is useful when
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to Firebase Auth. sounds good enough I think. The word database in there might confuse somebody.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

src/auth/auth.ts Outdated
* When importing a list of password users, UserImportOptions are required to be specified.
*
* @param {UserImportRecord[]} users The list of user records to import to Firebase Auth
* database.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove database from here too?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

this.indexMap = {};

this.validateAndPopulateUsers();
this.validateAndPopulateOptions();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we refactor the code so the above two lines end up looking like:

this.validatedUsers = this.validateUsers(users);
this.validatedOptions = this.validateOptions(options);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not as simple as you think, there are other private variables computed in the process of these. I prefer not to refactor this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, i made that change but it contains private variables within.

passwordHash: undefined,
salt: undefined,
lastLoginAt: undefined,
createdAt: undefined,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we not list these undefined fields here? Then you don't have to remove them later in the code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I add them so i don't have to define this interface. anyway, i can remove them but will need to define this interface. In addition, other fields could be undefined as passed from developer and still need to be removed. Anyway, i will remove and define the interface even though there is little gained from this.

}


/** Callback function to validate an object. */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Document the contract of this function in more details. What should it do when an invalid user is encountered?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

}

/**
* Imports the list of users provided to Firebase Auth users database. This is useful when
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets drop database from the text. Here and elsewhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

* Returns the corresponding constructed uploadAccount request.
* @return {UploadAccountRequest} The constructed uploadAccount request.
*/
public generateRequest(): UploadAccountRequest {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think buildRequest is a better name for this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

return safeDelete(randomUid);
});

it('successfully imports users with HMAC_SHA256 hashed passwords', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to organize these tests by creating an array of test fixtures? For example:

interface UserImportTest {
  name string
  importOptions UserImportOptions
  passwordHash string
}

const fixtures: Array<UserImportTest> = [
  {
     name: 'hmac',
     importOptions: {...},
     passwordHash: crypto.createHmac(...),
  },
  ...
]; 

fixtures.forEach((fixture) => {
   it(...)
});

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, changing it as described. The passwordHash will be a function so i don't need to hardcode the parameters.

photoURL: 'http://www.example.com/' + newUserUid + '/photo.png',
disabled: false,
};
let deleteQueue = Promise.resolve();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I overwrite this at the bottom every time I queue a single delete:

  // Suppress errors in delete queue to not spill over to next item in queue.
  deleteQueue = deletePromise.catch((error) => {
    // Do nothing.
  });
  return deletePromise;

* @return {UserImportResult} The user import result based on the returned failed
* uploadAccount response.
*/
public generateResponse(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buildResponse

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@googlebot
Copy link

We found a Contributor License Agreement for you (the sender of this pull request), but were unable to find agreements for the commit author(s). If you authored these, maybe you used a different email address in the git commits than was used to sign the CLA (login here to double check)? If these were authored by someone else, then they will need to sign a CLA as well, and confirm that they're okay with these being contributed to Google.
In order to pass this check, please resolve this problem and have the pull request author add another comment and the bot will run again. If the bot doesn't comment, it means it doesn't think anything has changed.

Copy link
Contributor

@hiranya911 hiranya911 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks pretty good. Just a few nits to address, and then I can LGTM.

src/auth/auth.ts Outdated
/**
* Imports the list of users provided to Firebase Auth. This is useful when
* migrating from an external authentication system without having to use the Firebase CLI SDK.
* A maximum list of 1000 users are allowed to be imported one at a time.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: At most 1000 users are....

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

};
public static INVALID_HASH_ALGORITHM = {
code: 'invalid-hash-algorithm',
message: 'The hash algorithm must be a string matching the list of supported algorithms.',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: The hash algorithm must match one of the strings in the list of...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

computePasswordHash: (userImportTest: UserImportTest): Buffer => {
return Buffer.from(scryptPasswordHash, 'base64');
},
rawPassword: scryptRawPassword,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can these two also be password and NaCl like the other cases? In that case you can extract these fields out of the fixture and just make them shared constants.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I modified the tests to all use the same rawPassword and rawSalt constants. However, it is not easy to generate the scrypt (internal version) password hash on the fly. The only way to do it is via: https://github.com/firebase/scrypt

}

/**
* Safe deletes a specificed user identified by uid. This API chains all delete
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Safely...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

cpuMemCost?: number;
parallelization?: number;
blockSize?: number;
dkLen?: number;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I missed this in the previous review. Shall we spell this out like all the others?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the backend name. We call this derivedKeyLength (the developer facing one). Check UserImportOptions inteface.

return;
private populateOptions(
options: UserImportOptions, requiresHashOptions: boolean): UploadAccountOptions {
let populatedOptions: UploadAccountOptions = {};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be just let populatedOptions: UploadAccountOptions;?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually return this when hash options are required but that is fine, i will return {} directly.

}
index++;
});
return populatedUsers;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you wanted, you can just return a single array from this method containing both users and errors:

const populatedUsers = [];
users.forEach((user, index) => {
  try {
    const result = populateUploadAccountUser(user, userValidator);
    populatedUsers.push(result);
  } catch (error) {
    populatedUsers.push({index, error});
  }
})
return populatedUsers;

Then in the caller (constructor) you can make all the state changes:

const results = populateUsers(...);
this.validatedUsers = // filter out the users from results
this.indexMap = // construct the index map
this.requireHashOptions = // check if at least one user requires hash

I don't feel strongly about this, so I'll leave it to you.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not easy constructing the indexMap after this completes. I have to remember the map original locations of the users to their new locations in the request sent to backend.

let index = 0;
this.users.forEach((user) => {
const populatedUsers: UploadAccountUser[] = [];
users.forEach((user) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could do users.forEach((user, index) => { here and avoid having to maintain you own index variable.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@hiranya911
Copy link
Contributor

If you wish to amend the author of old commits, here's a useful guide: https://www.git-tower.com/learn/git/faq/change-author-name-email

You basically has to perform an interactive rebase, and amend each commit with the incorrect email.

Copy link
Contributor

@hiranya911 hiranya911 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@hiranya911
Copy link
Contributor

Related to #158

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants