firebase-admin-node icon indicating copy to clipboard operation
firebase-admin-node copied to clipboard

Fix type for cert(...) function, to support standard downloadable serviceAccount.json

Open charles-allen opened this issue 5 years ago • 14 comments

Motivation

The JavaScript implementation of cert(...) already supports snake_case account objects. I suspect this is the most popular format (for non-Cloud-Function usage) since GCP & Firebase console provide downloadable account configs as snake_case.

Every single TypeScript client of the Node Admin SDK must navigate this type error, which occurs at the very start of their SDK journey. It has already been raised as an issue 3 separate times. It's a trivial fix that increases the accuracy & readability of the codebase.

Issues

  • Resolves https://github.com/firebase/firebase-admin-node/issues/522
  • Resolves https://github.com/firebase/firebase-admin-node/issues/624
  • Resolves https://github.com/firebase/firebase-admin-node/issues/903

Testing

  • Make sure all existing tests pass => crosses fingers
  • If you fixed a bug ... add a new test => n/a (compile-time)

API Changes

  • This isn't a change; rather the API is already supporting snake_case and the TypeScript description is simply deficient

charles-allen avatar Dec 07 '20 04:12 charles-allen

Thanks for your pull request. It looks like this may be your first contribution to a Google open source project (if not, look below for help). Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

:memo: Please visit https://cla.developers.google.com/ to sign.

Once you've signed (or fixed any issues), please reply here with @googlebot I signed it! and we'll verify it.


What to do if you already signed the CLA

Individual signers
Corporate signers

ℹ️ Googlers: Go here for more info.

google-cla[bot] avatar Dec 07 '20 04:12 google-cla[bot]

@googlebot I signed it!

charles-allen avatar Dec 07 '20 04:12 charles-allen

We found a Contributor License Agreement for you (the sender of this pull request), but were unable to find agreements for all the commit author(s) or Co-authors. 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 then comment @googlebot I fixed it.. If the bot doesn't comment, it means it doesn't think anything has changed.

ℹ️ Googlers: Go here for more info.

google-cla[bot] avatar Dec 31 '20 13:12 google-cla[bot]

@googlebot I fixed it

charles-allen avatar Jan 01 '21 00:01 charles-allen

Been testing this solution and seems alright to me. I was a bit concerned about tsc potentially flagging an object that has more properties than the declared interface (since service account json contains many other fields), but that doesn't seem to be the case. It still needs to go through our internal API review process. I'll find some time for that in the coming weeks.

hiranya911 avatar Jan 22 '21 23:01 hiranya911

Hi @hiranya911,

I PR'd this implementation because it's a mirror of the existing camelCase interface (and I thought that would be the smallest possible increment).

More complete & explicit

If you're willing or even prefer to make a larger change, I think the best option is an interface that explicitly matches the expectations of the API (i.e everything that's required from a snake_case JSON would be marked required, and all the optional properties would be listed & explicitly marked optional):

export interface ServiceAccountJson {
  // I think these are all the required fields
  client_email: string // required
  private_key: string // required
  project_id: string // required
  // I think these are all the optional fields
  auth_provider_x509_cert_url?: string
  auth_uri?: string
  client_id?: string
  client_x509_cert_url?: string
  private_key_id?: string
  token_uri?: string
  type?: string
}

Other options

You could also use an index signature, but this opens the door for typos in any optional property (typos in required properties will get caught); personally I am not a fan:

export interface ServiceAccountJson {
  project_id: string // required
  private_key: string // required
  client_email: string // required
  [additional: string]: string // optional
}

Internal usage

If you need different "optional" status internally, you can do a lot with TypeScript:

// Example: types for merging passed JSON with default values
type ServiceAccountJsonDefaults = Required<Pick<ServiceAccountJson, 'type' | 'client_id'>>
type ServiceAccountJsonMerged = ServiceAccountJson & ServiceAccountJsonDefaults

const defaults: ServiceAccountJsonDefaults = { type: '...', client_id: '...' };
const merged: ServiceAccountMerged = Object.assign(defaults, serviceAccount);

Testing

I wrote previously that I couldn't write a test, but actually I think we could add a small dummy TypeScript program that imports the SDK (like the cases below), and add a step to the GitHub actions workflow to compile that program (compilation failure would fail the build).

You mentioned:

I was a bit concerned about tsc potentially flagging an object that has more properties than the declared interface (since service account json contains many other fields)

From the TypeScript docs - TL;DR: the "additional property check" is only when assigning/passing an object literal:

Object literals get special treatment and undergo excess property checking when assigning them to other variables, or passing them as arguments. If an object literal has any properties that the “target type” doesn’t have, you’ll get an error

There might be cases where a client constructs their config inline (e.g. case 3) where the currently proposed PR might fail as you described (I believe the more complete interface at the top of this comment would pass):

// case 1: static import
// - type inferred from JSON itself => checked structurally vs the interface
// - not passing a literal => excess property check skipped
import serviceAccount from 'service-account.json'
admin.initializeApp({ credential: admin.credential.cert(serviceAccount) })

// case 2: dynamic import
// - type asserted => actual JSON is never checked
// - not passing a literal => excess property check skipped
const serviceAccount: ServiceAccountJson = await import(configPath) // or require(...)
admin.initializeApp({ credential: admin.credential.cert(serviceAccount) })

// case 3: inline literal
// - type inferred from the literal => checked structurally vs the interface
// - passing a literal => excess property check might fail
admin.initializeApp({ credential: admin.credential.cert({
  project_id = process.env.FIREBASE_PROJECT_ID,
  private_key = process.env.FIREBASE_PRIVATE_KEY,
  client_email = process.env.FIREBASE_PROJECT_ID,
  another_property = process.env.FIREBASE_ANOTHER_PROPERTY // might fail on this line
}) })

charles-allen avatar Jan 23 '21 01:01 charles-allen

I wrote previously that I couldn't write a test, but actually I think we could add a small dummy TypeScript program that imports the SDK (like the cases below), and add a step to the GitHub actions workflow to compile that program (compilation failure would fail the build).

I think you can (and should) add some unit tests to the existing test suite. Something similar to:

https://github.com/firebase/firebase-admin-node/blob/1862342636e816b61337262254cdfe0cc410c640/test/unit/firebase.spec.ts#L114-L117

You can convert the mocks.certificateObject into an instance of ServiceAccountJson and pass that into cert.

hiranya911 avatar Jan 25 '21 19:01 hiranya911

I think you can (and should) add some unit tests to the existing test suite. Something similar to: ...

I'll see if I can add something along those lines that verifies a snake_case config can be imported in all the standard ways, passed to cert, and most importantly:

  1. that tsc is happy about it (the tests will fail the build step if this isn't the case)
  2. that cover the excess property case

charles-allen avatar Jan 26 '21 08:01 charles-allen

Sorry for the delay. Busy with work. I should have time to revisit this this week :)

charles-allen avatar Mar 22 '21 23:03 charles-allen

@charles-allen any update on this? I encountered this issue just this week and it was bugging the heck out of me, pardon the pun!

paul-uz avatar Nov 26 '21 10:11 paul-uz

@charles-allen any update on this? I encountered this issue just this week and it was bugging the heck out of me, pardon the pun!

I got sidetracked by work. But the next 2 weeks is the perfect time for me to revive it. I think I just need to copy the existing tests and swap camel for snake case.

Can't offer any guarantees on how much more time it would take to get accepted though.

charles-allen avatar Nov 26 '21 11:11 charles-allen

@charles-allen no worries :)

paul-uz avatar Nov 26 '21 11:11 paul-uz

@charles-allen no worries :)

Thanks for bumping it :) Good to get in back in my peripheral vision!

charles-allen avatar Nov 26 '21 11:11 charles-allen

it's been almost 2 years, any update?

tylim88 avatar Jul 20 '22 13:07 tylim88

it's been almost 2 years, any update?

I'm not working with Firebase any more, and I think Hiranya has left the Firebase team. This PR is probably horribly out-of-date. You're welcome to replace it with a new PR :)

charles-allen avatar Oct 11 '22 20:10 charles-allen