kevlened / fireway

A schema migration tool for firestore
MIT License
279 stars 42 forks source link

How to access admin.auth()? #34

Open Acterion opened 3 years ago

Acterion commented 3 years ago

Is it even possible to access the admin object from the migration script?

I want to change some filed names inside customUserClaims, so I thought it would be nice to create a migration for that. But for doing that I would need to access admin.auth().listUsers() and then run setCustomUserClaims() for each individual user.

I've added const admin = require("firebase-admin"); before the script, which amazingly worked, but that feels like a workaround and not a proper solution.

I would suggest adding admin object to MigrateOptions

vandres commented 3 years ago

Would also like to know, what the way to go is, when needing auth data.

kiptoomm commented 3 years ago

I have the same need.

For me, the following error occurs:

{
  errorInfo: {
    code: 'auth/invalid-credential',
    message: 'Failed to determine project ID for Auth. Initialize the SDK with service account credentials or set project ID as an app option. Alternatively set the GOOGLE_CLOUD_PROJECT environment variable.'
  },
  codePrefix: 'auth'
}

What I did:

  1. tell fireway that we're using the local emulator: export FIRESTORE_EMULATOR_HOST="localhost:8080"
  2. run the migration: fireway migrate

What to do to get the firebase-admin functions working with Fireway?

By the way, I came across this package (looks like a fireway fork?) that seems to support --emulator as an option. FWIW, it seems to me like a neater solution.

kevlened commented 3 years ago

I'd accept a PR for an --emulator option!

I'd rather expose the result of admin.auth() directly. This provides the ability to do a --dryrun for auth in the future. I would also accept PRs for that, assuming there were tests.

kiptoomm commented 3 years ago

I'd rather expose the result of admin.auth() directly

I'm not sure I understand this.

FWIW, here's a snapshot of the migration script that produces the error I shared above:

// v0.0.2_set_custom_claims.js

const { listAllUsers } = require("./helpers");

/**
 * set custom claims on all firebase auth users
 * */

module.exports.migrate = async ({ firestore }) => {
  /**
   * set the custom claims for all users
   */
  const setCustomClaims = async () => {
    // fetch all user entries from firebase auth
    let firebaseAuthUsers = [];
    await listAllUsers(firebaseAuthUsers); // firebaseAuthUsers is a List<JSON> representing the auth user objects
    console.log("firebaseAuthUsers: ", firebaseAuthUsers);

    // for each auth user:
    for (authUser in firebaseAuthUsers) {
      // set auth custom claims ... 
    }
  };

  // run the function
  setCustomClaims();
};

// helpers.js

const admin = require("firebase-admin");
/**
 * enumerates all [firebase auth] users from the firebase project
 * adapted from https://firebase.google.com/docs/auth/admin/manage-users#list_all_users
 *
 * @param {List<JSON>} usersList empty list to append the results into
 * @param {string} nextPageToken
 * @returns {List<JSON>} a list of JSON objects representing users
 *
 */
exports.listAllUsers = function(usersList, nextPageToken) {
    // List batch of users, 1000 at a time.
    return admin
        .auth()
        .listUsers(1000, nextPageToken)
        .then((listUsersResult) => {
            listUsersResult.users.forEach((userRecord) => {
                usersList.push(userRecord.toJSON());
            });

            if (listUsersResult.pageToken) {
                // List next batch of users.
                return listAllUsers(usersList, listUsersResult.pageToken);
            }

            // the compiled list, after all the batches are fetched
            return usersList;
        })
        .catch((error) => {
            console.log('Error listing users:', error);
        });
};
kevlened commented 3 years ago

I'd rather expose the result of admin.auth() directly

I'm not sure I understand this.

I want to reduce surface area, because in my experience, more surface area is more that can break. If you need auth, fireway can expose auth.

FWIW, here's a snapshot of the migration script that produces the error I shared above:

If it runs against localhost, but not remotely, continue to do what you're doing now (const admin = require("firebase-admin")), but configure it with your remote credentials. Ideally, this would be handled nicely by fireway, but isn't right now.

kiptoomm commented 3 years ago

If you need auth, fireway can expose auth.

It'd be nice to see how to use the fireway-provided auth.

If it runs against localhost, but not remotely, continue to do what you're doing now

It doesn't work on localhost right now (the error I shared is from localhost - 'Failed to determine project ID for Auth . Initialize the SDK with service account credentials or set project ID as an app option. Alternatively set the GOOGLE_CLOUD_PROJECT environment variable.').

To rephrase the issue: I don't know how else on localhost (besides setting the env variable) to tell fireway that I am using the Firebase emulator and that I don't have a real project ID or other credentials since I'm using the emulator.

Thanks for your patience, I am new to JS/NoSQL :)

kevlened commented 3 years ago

If you need auth, fireway can expose auth.

It'd be nice to see how to use the fireway-provided auth.

It doesn't work today, but ideally your migration file could look something like:

module.exports = async ({ firestore, auth }) => {
  // your migration logic
};

If it runs against localhost, but not remotely, continue to do what you're doing now

It doesn't work on localhost right now (the error I shared is from localhost - 'Failed to determine project ID for Auth . Initialize the SDK with service account credentials or set project ID as an app option. Alternatively set the GOOGLE_CLOUD_PROJECT environment variable.').

To rephrase the issue: I don't know how else on localhost (besides setting the env variable) to tell fireway that I am using the Firebase emulator and that I don't have a real project ID or other credentials since I'm using the emulator.

Ah. An --emulator flag would make this less painful. For now, manually using the FIRESTORE_EMULATOR_HOST env variable is probably your best bet. There might be a way to use @firebase/testing, but I haven't explored that.

Thanks for your patience, I am new to JS/NoSQL :)

No worries! Happy to help.

kiptoomm commented 3 years ago

For now, manually using the FIRESTORE_EMULATOR_HOST env variable is probably your best bet

It normally works for me but it's not working for me in this particular use case (i.e., when using the firebase admin SDK's modules - auth in this case) :(

I have also tried running the script against a remote database by providing the --projectId option (and unsetting FIRESTORE_EMULATOR_HOST) but I still get the same error

dsvgit commented 2 years ago

app property which is provided by fireway seems like works for me

const clearClaims = async (auth, authUser) => {
  if (authUser.customClaims) {
    return await auth.setCustomUserClaims(authUser.uid, null);
  }
};

const updateClaims = async (auth, authUser, changes) => {
  const newCustomClaims = { ...authUser.customClaims, ...changes };
  return await auth.setCustomUserClaims(authUser.uid, newCustomClaims);
};

module.exports.migrate = async ({ firestore, app }) => {
  const auth = app.auth();

  const usersSnapshot = await firestore.collection("users").get();
  usersSnapshot.forEach(async (snapshot) => {
    const userId = snapshot.id;
    try {
      const authUser = await auth.getUser(userId);

      // await clearClaims(auth, authUser);
      // console.log(authUser.customClaims);

      await updateClaims(auth, authUser, { admin: true });
    } catch (error) {
      // console.log(error, userId);
    }
  });
};
xd2 commented 2 years ago

Hi. In my case, the fireway app.auth() (v1.0.2) was failing using emulator even with --forceWait as suggested by @dsvgit (even with the --dryRun switch). I was getting the following error

FirebaseAuthError: Failed to determine project ID for Auth. Initialize the SDK with service account credentials or set project ID as an app option. Alternatively set the GOOGLE_CLOUD_PROJECT environment variable. at FirebaseAuthError.FirebaseError [as constructor] hereunder my command line to start the migration FIRESTORE_EMULATOR_HOST=localhost:8080 yarn fireway migrate --require=ts-node/register --forceWait --dryRun

I end up configuring my env using these two variables:

GCLOUD_PROJECT=myproject FIREBASE_AUTH_EMULATOR_HOST=localhost:9099

Local migration using 'app.auth' works properly with the emulator now !