Repository: streamlit/streamlit

Issue:
Don't ship mapbox token in the code
Instead of shipping our mapbox token with our codebase, for better future-proofing it's a good idea to fetch that token from a some place online. This way we can cancel/change the token any time we want.

For example, we could just add it to a publically-accessible S3 bucket at `https://streamlit.io/tokens.json` and just have the frontend fetch that token when it first encounters a DeckGlChart element (and cache it in memory).

Of course, this should only happen if the user didn't configure Streamlit to use a custom token.


Discussion:
Related: https://docs.google.com/document/d/1mGkuIpg98XGPozTILtaSkuONh9_Tp-N_oBGvCFQly0E/edit

Resolving patch:
diff --git a/lib/streamlit/config.py b/lib/streamlit/config.py
--- a/lib/streamlit/config.py
+++ b/lib/streamlit/config.py
@@ -548,8 +548,7 @@ def _browser_server_port():
                 which has limitations and is not guaranteed to always work.
                 To get a token for yourself, create an account at
                 https://mapbox.com. It's free! (for moderate usage levels)""",
-    default_val="pk.eyJ1IjoidGhpYWdvdCIsImEiOiJjamh3bm85NnkwMng4M3"
-    "dydnNveWwzeWNzIn0.vCBDzNsEF2uFSFk2AM0WZQ",
+    default_val="",
 )
 
 


Test patch:
diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx
--- a/frontend/src/App.test.tsx
+++ b/frontend/src/App.test.tsx
@@ -65,7 +65,7 @@ describe("App", () => {
       authorEmail: "ae",
       maxCachedMessageAge: 2,
       commandLine: "command line",
-      mapboxToken: "mpt",
+      userMapboxToken: "mpt",
     } as SessionInfoArgs)
     MetricsManager.current = getMetricsManagerForTest()
   })
diff --git a/frontend/src/hocs/withMapboxToken/MapboxToken.test.ts b/frontend/src/hocs/withMapboxToken/MapboxToken.test.ts
new file mode 100644
--- /dev/null
+++ b/frontend/src/hocs/withMapboxToken/MapboxToken.test.ts
@@ -0,0 +1,89 @@
+/**
+ * @license
+ * Copyright 2018-2020 Streamlit Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { MapboxToken, TOKENS_URL } from "hocs/withMapboxToken/MapboxToken"
+import { SessionInfo } from "lib/SessionInfo"
+import fetchMock from "fetch-mock"
+
+function setSessionInfoWithMapboxToken(userMapboxToken: string): void {
+  SessionInfo.current = new SessionInfo({
+    streamlitVersion: "sv",
+    pythonVersion: "pv",
+    installationId: "iid",
+    authorEmail: "ae",
+    maxCachedMessageAge: 2,
+    commandLine: "cl",
+    userMapboxToken: userMapboxToken,
+  })
+}
+
+describe("MapboxToken", () => {
+  beforeEach(() => {
+    setSessionInfoWithMapboxToken("")
+  })
+
+  afterEach(() => {
+    SessionInfo["singleton"] = undefined
+    MapboxToken["token"] = undefined
+    fetchMock.restore()
+  })
+
+  test("Returns cached token if defined", async () => {
+    MapboxToken["token"] = "cached"
+    await expect(MapboxToken.get()).resolves.toEqual("cached")
+  })
+
+  test("Returns userMapboxToken if non-empty", async () => {
+    const userToken = "nonEmptyToken"
+
+    setSessionInfoWithMapboxToken(userToken)
+    await expect(MapboxToken.get()).resolves.toEqual(userToken)
+
+    // The token should also be cached.
+    expect(MapboxToken["token"]).toEqual(userToken)
+  })
+
+  test("Fetches remote token if userMapboxToken is empty", async () => {
+    const remoteToken = "remoteMapboxToken"
+
+    fetchMock.get(TOKENS_URL, { mapbox: remoteToken })
+    await expect(MapboxToken.get()).resolves.toEqual(remoteToken)
+
+    // The token should also be cached.
+    expect(MapboxToken["token"]).toEqual(remoteToken)
+  })
+
+  test("Errors if remote token is missing", async () => {
+    fetchMock.get(TOKENS_URL, { ohNo: "noTokenHere" })
+
+    await expect(MapboxToken.get()).rejects.toEqual(
+      new Error(`Missing token "mapbox" (${TOKENS_URL})`)
+    )
+
+    // No cached token after failure.
+    expect(MapboxToken["token"]).toBeUndefined()
+
+    fetchMock.restore()
+    fetchMock.get(TOKENS_URL, 404)
+    await expect(MapboxToken.get()).rejects.toEqual(
+      new Error(`Bad status: 404 (${TOKENS_URL})`)
+    )
+
+    // No cached token after failure.
+    expect(MapboxToken["token"]).toBeUndefined()
+  })
+})
diff --git a/frontend/src/hocs/withMapboxToken/withMapboxToken.test.tsx b/frontend/src/hocs/withMapboxToken/withMapboxToken.test.tsx
new file mode 100644
--- /dev/null
+++ b/frontend/src/hocs/withMapboxToken/withMapboxToken.test.tsx
@@ -0,0 +1,71 @@
+/**
+ * @license
+ * Copyright 2018-2020 Streamlit Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from "react"
+import { shallow } from "enzyme"
+import { MapboxToken } from "./MapboxToken"
+
+import withMapboxToken from "./withMapboxToken"
+
+interface TestProps {
+  label: string
+  mapboxToken: string
+}
+
+class TestComponent extends React.PureComponent<TestProps> {}
+
+function waitOneTick(): Promise<void> {
+  return new Promise(resolve => {
+    setTimeout(resolve)
+  })
+}
+
+describe("withMapboxToken", () => {
+  function getProps(): any {
+    return { label: "label" }
+  }
+
+  // Install a mock token in our token fetcher so that we don't hit
+  // the network.
+  beforeEach(() => {
+    MapboxToken["token"] = "mockToken"
+  })
+
+  afterEach(() => {
+    MapboxToken["token"] = undefined
+  })
+
+  it("renders without crashing", async () => {
+    const props = getProps()
+    const WrappedComponent = withMapboxToken(TestComponent)
+    const wrapper = shallow(<WrappedComponent {...props} />)
+
+    expect(wrapper.html()).not.toBeNull()
+  })
+
+  it("passes mapboxToken to wrapped component", async () => {
+    const props = getProps()
+    const WrappedComponent = withMapboxToken(TestComponent)
+    const wrapper = shallow(<WrappedComponent {...props} />)
+
+    // Wait one tick for our MapboxToken promise to resolve
+    await waitOneTick()
+
+    expect(wrapper.props().label).toBe("label")
+    expect(wrapper.props().mapboxToken).toBe("mockToken")
+  })
+})
diff --git a/frontend/src/lib/MetricsManager.test.ts b/frontend/src/lib/MetricsManager.test.ts
--- a/frontend/src/lib/MetricsManager.test.ts
+++ b/frontend/src/lib/MetricsManager.test.ts
@@ -26,7 +26,7 @@ beforeEach(() => {
     authorEmail: "ae",
     maxCachedMessageAge: 2,
     commandLine: "command line",
-    mapboxToken: "mbx",
+    userMapboxToken: "mbx",
   })
 })
 
diff --git a/lib/tests/streamlit/config_test.py b/lib/tests/streamlit/config_test.py
--- a/lib/tests/streamlit/config_test.py
+++ b/lib/tests/streamlit/config_test.py
@@ -355,7 +355,7 @@ def test_check_conflicts_browser_serverport(self):
             config._check_conflicts()
         self.assertEqual(
             str(e.value),
-            "browser.serverPort does not work when global.developmentMode is " "true.",
+            "browser.serverPort does not work when global.developmentMode is true.",
         )
 
     def test_check_conflicts_s3_sharing_mode(self):