WorryFree Computers   »   [go: up one dir, main page]

Engineering Manager
Peter Friese
Developer Advocate

Spark new ideas with Experimental Extensions

Firebase Extensions are a convenient way to add new functionality to your app with the click of a button. They are pre-packaged solutions for common problems that you can easily add to your Firebase app. Extensions are based on Firebase and Google Cloud products that you already know and use.

On the Firebase Extensions page, you will find a collection of official extensions for features that developers find themselves in need of regularly, such as resizing images, sending emails based on the contents of Firestore documents, translating text, shortening URLs, and more. Most of these extensions have been built and tested by the Firebase team, and follow best practices. In addition, we have teamed up with partners like Stripe to bring you extensions for processing payments and sending invoices.

Installing extensions is easy via the Firebase console or the Firebase command line interface.

Firebase Experimental Extensions

To allow us to get more feedback from you and to better understand which use cases and extension features people are most interested in, we are launching a new type of extensions: Experimental Extensions.

These are fully functional extensions that we think you'll find useful, but they haven't yet gone through the same amount of testing and polish as the official published extensions.

The following extensions are built by our engineers looking for feedback from the community.

  • Shortening links using Firebase Dynamic Links
  • Performing sentiment analysis on text in Firestore documents
  • Adding custom claims to Firebase Auth users
  • Scheduled writes to Firestore documents
  • ... and more coming soon!

To install an experimental extension, navigate to the extension's README file in the Firebase Experimental Extensions repo on GitHub, and then click on the Install on Firebase button. This will take you to the Firebase Console, where you can then select the project you'd like to install this extension in.

image of installing experimental extension

You can also install extensions via the command line. To do so, make sure you've got the Firebase Command Line tools installed, then run the following command:

$ firebase ext:install firestore-shorten-urls-dynamic-links --project=(your project ID)

This command will install the Shorten URLs with Dynamic Links experimental extension in your project.

We love to hear your feedback, and we're looking forward to seeing what you will build using the new experimental extensions! Use the GitHub issue tracker of the experimental extensions repository to leave feedback.

If you'd like to submit PRs to improve an experimental extension, you will need access to the extension authoring tools. To learn more about this, join our Alpha program.

Community Extensions

From conversations with you, we know you're excited to build and share your own extensions.

To share extensions with other developers, you need to become an Extension Publisher. Register as a publisher by joining the Firebase Alpha program, and indicate you're interested in building Community Extensions in the comments field. You can then publish your extensions under your publisher ID.

Your extension code will live in Firebase, so other alpha group members can install your extension directly from the command line.

At the moment, Community Extensions can only be shared with members of the Firebase Alpha program, but we're working hard to let you share your extensions publicly.

To extend where no Firebaser has gone before

We're excited to share these two new kinds of extensions with you, and we can't wait to see what you're going to build!

Software Engineer

Image of comic

We made a comic about Crashlytics—the first ever comic created by Firebase! The Great Crash Detective investigates a mystery case of a new crash in her mobile application. Does she solve it, or let her product manager down? Read it to find out!

The Investigation

The inspiration for this comic all started with a customer interview. On the Crashlytics team, we love talking to developers about how they're using our product. So naturally when a developer reached out to us to find out if there were ways to better understand the events leading up to a crash, we jumped on the task. Crashlytics allows you to add custom logs to your crash data. However, this developer needed help searching through these custom logs to find what events these crashes were occurring within.

The Page Turner

In August of 2018, Crashlytics released the ability to export your data to BigQuery and enhanced it last year by making it possible to export data in realtime. One of the useful things about exporting your crashes to BigQuery is that you can freely investigate the underlying crashes occurring in your app.

We asked the developer who was interested in searching their custom logs if they had considered exporting their data to BigQuery, they explained that they didn't have bandwidth to learn how to use SQL.

Image of comic

Learning SQL is a continuous process

This was a huge opportunity for us to communicate with customers the alternatives to using SQL (though, we also think exploring your BigQuery data using SQL is pretty great!). For example, Google Data Studio lets you visualize your BigQuery data quickly and easily. You can use our template to customize a report to address your specific needs.

The Mystery Solved

The developer was able to use a Google Data Studio report to visualize their crash reports and analyze their data quickly and easily. They had already followed the steps to enable BigQuery export and the instructions to attach a datasource to our Data Studio Template to create a report, the steps to adding a custom log filter were quick and easy:

First, they navigated to the "Crashes by File" page of the report.

Image showing menu of Crashes by File

Next, they clicked Insert -> Advanced Filter.

Image with arrow pointing to Advanced Filter

Afterwards, they placed the filter in an empty spot in the report.

Image with arrow pointing toward where to place filter

Finally, they edited the control field for this filter. On the data pane (to the right of the report), they changed the control field to "logs.message".

Image with arrow pointing towards control field

That's all that was needed to search a Crashlytics crash dataset for the occurrence of custom log events (here's an example template of the above steps). Searching by other fields within Crashlytics crashes, such as analytics breadcrumbs, is as easy as changing the control field for that filter.

Closing the Case

We'd love to hear your feedback. Should we do more Firebase comics? Are you interested in what happens next for the Great Crash Detective? Do you have any feedback about using Crashlytics with BigQuery and Data Studio? Feel free to reach out to us on Twitter or come hang out with us in the Firebase Community Slack channel.

Image of comic

Did we make a comic about Crashlytics, or was it all a dream?

This comic was created by Luke Kruger-Howard, an Ignatz-nominated comic artist, in collaboration with myself and other engineers and product folks from the Crashlytics team.

p.s. Why did the horse's app crash?

Because the app was unstable.

Developer Programs Engineer

Firebase Security Rules gate your user's access to and enforce validations for Firestore, Firebase Storage, and the Realtime Database. It's important to code review the Security Rules, just like you code review the application code. Because these rules are written in a domain-specific language, that puts some people in the position of code reviewing something they don't feel like they understand.

If you're finding yourself in that position, don't worry! This post will walk through how to approach reviewing and giving good feedback on Security Rules. The examples will be from Firestore Security Rules, and are mostly applicable to Storage as well. If you're reviewing Realtime Database Security Rules, although these principles apply, the rules use a different language, so the examples will be different.

Watch out for global rules

When you're looking at Security Rules, check first for any top level rules that apply to everything. A document can match multiple rules, and if any rule grants access, access is granted. In the example below, there's a specific rule that only grants authors access to post documents, but the global rule lets anyone on the internet read or write to any place in your database:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if true;
    }
    match /posts/{post} {
      allow read, write: if request.resource.data.authorUID == request.auth.uid ;
    }
  }
}

It's important to look for the global match statement match /{document=**} throughout the entire rules file, not just at the top. If there are thorough rules but also a global match statement, universal access will still be granted through the global match statement.

There are a very small number of valid use cases for global match statements. For example, granting access to admin users:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if isAdmin();
    }
  }
}

If you find a global rule that is read, write: if true, you can stop your review and ask them to fix it. If you find any other condition on a global rule, make sure it makes sense for your application.

Personally Identifiable Information should be separated

The next thing to do is to look at the data that you're storing. What is Personally Identifiable Information (PII) that should only be accessed by that particular user?

Security Rules apply to entire documents, not just fields. You should be able to look at each kind of document that you have and describe which kind of users should be able to read it, create it, update it, and delete it.

If you find documents where it's fine for most of the document to be read by anyone, but a few fields need to remain private, break those fields into their own document; the easiest way to do this is usually to create a subcollection off the existing document.

Now check the Security Rules for each of the documents that you identified as containing PII. The best practice for PII is that it is keyed by the user's ID, and only that user is allowed access:

match /secrets/{uid} {
  allow create, update: if request.auth.uid == uid;
}

This pattern only works if there's a max of one document in the collection for each user. For cases of multiple documents per user, you could create subcollections of a user document:

match /users/{uid}/secrets/{secret} {
  allow create, update: if request.auth.uid == uid;
}

Or you could include the id of the user who should have access as an attribute, and restrict access to that user:

match /secrets/{secret} {
  allow create, update: if request.auth.uid == request.resource.data.uid;
}

Ideally, you would check that each document is as locked down as possible, but if you have limited time, the most important thing to check is that PII lives in separate documents and those PII documents are only accessible to that user.

Check the unit tests for the Security Rules

Just like application changes should come with test changes, so should Security Rules. Security Rules tests run against the Firebase Emulator Suite, and can be included in your CI setup.

If you're not yet testing Security Rules, check out this documentation to get started, this video for an overview of testing, and this blog post and video on adding your tests to CI.

If the Security Rules are fully tested, there should be a lot of tests. For each kind of document, there are usually four different kinds of access: read, create, update, and delete. (These are just the most common permissions to grant; create, update, and delete can be rolled up into write, and read can be broken into get and list.) For each permission, there should be a test of the happy path, granting permission, and a test for each situation that should deny access.

Say I have one rule:

// firestore.rules

allow update: if
  // User is the author
  resource.data.authorUID == request.auth.uid &&
  // `authorUID` and `createdAt` are unchanged
  request.resource.data.diff(request.resource.data).unchangedKeys().hasAll([
    "authorUID",
    "createdAt"
  ]) &&
  // Title must be < 50 characters long
  request.resource.data.title.size < 50;

To completely test this, I would write tests around granting access and each way that access could be denied:

// test.js

const firebase = require("@firebase/rules-unit-testing");

const dbAuthorAuth = firebase.initializeTestApp({
  projectId: TEST_FIREBASE_PROJECT_ID,
  auth: {
    uid: "author",
    email: "alice@example.com"
  }
}).firestore();

const dbOtherAuth = firebase.initializeTestApp({
  projectId: TEST_FIREBASE_PROJECT_ID,
  auth: {
    uid: "other",
    email: "otto@example.com"
  }
}).firestore();


describe("blog posts", () => {

  before(async () => {
    dbAuthorAuth.doc("drafts/12345").set({
      authorUID: "author",
      createdAt: Date.now(),
      title: "Make an apple",
      content: "TODO!"
    });
  });

  it("can be updated by author if immutable fields are unchanged", async () => {
    await firebase.assertSucceeds(dbAuthorAuth.doc("drafts/12345").update({
      title: "Make an app",
      content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
    }));
  });

  it("cannot be updated by anyone other than the author", async () => {
    await firebase.assertFails(dbOtherAuth.doc("drafts/12345").update({
      title: "Make an app",
      content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
    }));
  });

  it("cannot be updated by author if the author ID is changed", async () => {
    await firebase.assertFails(authorDb.doc("drafts/12345").update({
      authorUID: "New Person"
      title: "Make an app",
      content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
    }));
  });

  it("cannot be updated by author if the created date is  changed", async () => {
    await firebase.assertFails(authorDb.doc("drafts/12345").update({
      createdAt: Date.now(),
      title: "Make an app",
      content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
    }));
  });

  it("cannot be updated if the title is over 50 characters", async () => {
    await firebase.assertFails(authorDb.doc("drafts/12345").update({
      title: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
      content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
    }));
  });

As the reviewer, look through their test cases, and make sure they have tested each kind of document. If they've written good test descriptions, that's the easiest place to understand what the Security Rules are doing. Similarly, if they've written code comments in their rules, that can also be a good entry point to understanding the Security Rules.

Firestore only: Pay attention to subcollections

Subcollections don't inherit the rules from the parent document, so make sure they're specifically covered in the Security Rules. Look at the subcollections you're storing in Firestore; if all documents should have the same access that a parent document has, then that parent document should use the glob syntax: match /post/{id=**}.

What's more common is that a subcollection has been broken out into a subcollection because a different person needs to read it, or someone else is allowed to write to it. In that case, check the Security Rules to make sure it has its own match statement. Nesting is only a stylistic difference; whether you nest your match statements or not, make sure this is a match statement for the documents in the subcollections:

// Nested match statements are fine
match /post/{postID} {
  allow read: if ...
  match /comments/{commentID} {
    allow read: if ...
  }
}

// Unnested match statements are fine
match /post/{postID} {
  allow read: if ...
}
match /comments/{commentID} {
  allow read: if ...
}

If you're reviewing Realtime Database Security Rules, child nodes inherit from parent nodes, the opposite of the behavior in Firestore.

Check that validations are enforced

Security Rules can enforce type and data validations for specific fields in addition to preventing unwanted access. If you know that some documents have required fields, immutable fields, fields that must be a timestamp, or a field that must contain data in a specific range, check that those are enforced in the Security Rules for those documents. For example, in this rule, I'm checking that the immutable fields of authorUID, publishedAt, and url aren't changed by an update, and that the required fields of content, title, and visible are still present:

allow update: if
  // Immutable fields are unchanged
  request.resource.data.diff(request.resource.data).unchangedKeys().hasAll([
    "authorUID",
    "publishedAt",
    "url"
  ]) &&
  // Required fields are present
  request.resource.data.keys().hasAll([
    "content",
    "title",
    "visible"
  ]);

If you use the more granular permissions of create, update, and delete in place of write, make sure that validations apply to both create and update.

Admin SDKs bypass Security Rules

Because the Admin SDK authorizes using service account credentials, all requests from the Admin SDK, including Cloud Functions for Firebase, bypass Security Rules. To give a thorough security review for a Firebase app, it's also necessary to look at any other writes that are happening to your backend via the Admin SDK.

For example, if I have great Firestore Security Rules, but a Cloud Function exports data to a storage bucket that has no Security Rules, that would be a terrible way to treat my user's data. The flip side of this is that Cloud Functions can be used in situations where Security Rules don't work, for example, returning individual fields from documents. See this blog post for tips to use Cloud Functions in conjunction with Security Rules.