Search code examples
google-apps-scriptgoogle-drive-apihttp-status-code-403google-workspace

How to restrict an image used in an <img> tag in a Web App to "anyone within corporation"?


I can restrict the usage of the Web App but I cannot limit the access to the images referred in the <img> tag.

Now I am developing a Web application with Google Apps Script. This is an internal application of the corporation and I got to set the assets' access rights carefully.

For App itself, from Deploy as web app box of the script editor, I set Execute the app as Me (bar@foo.example.com) and Who has access to the app as Anyone within FOO corporation.

(Here let's say I belong to the FOO corporation. My Google account is bar@foo.example.com)

Now, when I login with the Google account of FOO, I could successfully access the Web App. But when I didn't login, I couldn't access it. That's what I want.

But when I set <img> tag to show the jpeg file in the Google Drive, I got to set the Share of the image file as Anyone on the internet with this link can view.

When I set <img> tag in the HTML in the Web App project and set the Share of the jpeg file as FOO Corporation -- Anyone in this group with this link can view, the <img> tag will not work (Error 403 will be returned).

I want to restrict the images' access right as well as the web app. How can I do it?


How to reproduce

put jpeg file

  1. put any JPEG (let's say it is baz.jpeg) file in the Google Drive
  2. right-click and select Share
  3. set the Get link as FOO Corporation -- Anyone in this group with this link can view
  4. double click baz.jpeg to preview it
  5. select => Open in new window
  6. let's say the URL was https://drive.google.com/file/d/xxxx_object ID_xxxx/view. record the object ID (here xxxx_object ID_xxxx)

  1. create a new untitled script project
  2. create an index.html file and add the code:
<html>

  <head>
    <base target="_top">
  </head>

  <body>
    <img src="https://drive.google.com/uc?id=xxxx_object ID_xxxx" width="30" height="30" alt="error alt text">
  </body>

</html>

create Code.gs

  1. change Code.gs as following code

    function doGet() {
      return HtmlService.createHtmlOutputFromFile('index');
    }

  1. Save it

publish it as Web App

  1. Select menu item Publish > Deploy as web app
  2. In Deploy as web app box, set Project version: as New
  3. Set Execute the app as: as Me (bar@FOO.example.com)
  4. Set Who has access the app: as Anyone within FOO corporation
  5. Click Deploy
  6. Click the text latest code in the message Test web app for your latest code

the result -- error

  • the webpage index.html is displayed
  • the image is not displayed. the icon of the broken picture is displayed
  • the alt text "error alt text" is displayed
  • if I right-click the icon of the broken picture and select Open in the new tab then it will open Error 403 (Forbidden) tab. The content is as follows:
    Google (logo)
    403. That’s an error.
    We're sorry, but you do not have access to this page. That’s all we know.

change the access right of the image file

  1. In Google Drive, find the image file baz.jpeg
  2. right-click and select Share
  3. set the Get link as Anyone on the internet with this link can view.
  4. Open the tab of the Web App with the broken picture icon and reload
  5. You see the correct image

What I want to do?

I want to set the access right of the image restricted as well as the Web app (only the user of FOO corporation can access). How can I do it?


Solution

  • 403 Forbidden

    The /uc endpoint, when the file permission is set to "in this group", returns a 403 Forbidden response even if you are logged in the G Suite account.


    Workaround

    You can implement a flow of dynamically appending an HTMLImageElement with src attribute set to image data (base-64 encoded string from bytes). With this, you can restrict access to both Web App and the image and still be able to load it.

    When deploying the Web App, make sure that the deployment has sufficient access to the file, for example, when the file has "Anyone in this group with this link can view" permissions:

    Execute the app as: Me
    Who has access to the app: Anyone within [org]

    Below is a small proof of concept, including a server-side utility and a sample HTML file.

    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
    </head>
    
    <body>
    
        <script>
    
            const asyncGAPIv2 = ({
                funcName,
                onFailure = console.error,
                onSuccess,
                params = []
            }) => {
                return new Promise((res, rej) => {
                    google.script.run
                        .withSuccessHandler(data => {
                            typeof onSuccess === "function" && onSuccess(data);
                            res(data);
                        })
                        .withFailureHandler(error => {
                            typeof onFailure === "function" && onFailure(error);
                            rej(error);
                        })
                    [funcName].apply(null, params);
                });
            };
    
            const fetchAndAppendImage = async ({ parent = document.body, id }) => {
    
                const data = await asyncGAPIv2({
                    funcName: "getImageFromDrive",
                    params: [{ id, token }]
                });
            
                const img = document.createElement("img");
                img.src = data;
    
                parent.append(img);
            };
    
            (async () => await fetchAndAppendImage({ id: "id here" }))();
        </script>
    
    </body>
    
    </html>
    

    You can pass the id to a server-side utility, get the file with getFileById (native authentication flow will ensure the requester will not get access to a file they do not have access to), and form an image data string by doing the following:

    1. Extract raw bytes from the File instance by changing getBlob to getBytes.
    2. Use the base64Encode method of the Utilities service to convert bytes to a base-64 encoded string and prepend data:image/png;base64, (Data URL scheme). If your images have another MIME-type, amend accordingly.
    /**
     * @summary gets an image from Google Drive
     * 
     * @param {{
     *  id : string
     * }}
     * 
     * @returns {string}
     */
    const getImageFromDrive = ({ id }) => {
    
        try {
          
          const file = DriveApp.getFileById(id);
          const bytes = file.getBlob().getBytes();
          
          const data = `data:image/png;base64,${Utilities.base64Encode(bytes)}`;
    
          return data;
    
        } catch (error) {
            console.warn(error);
            return "";
        }
    };
    

    Notes

    1. For the above to work, you must have the newer V8 runtime enabled.
    2. It is possible that the reason is being logged in into multiple accounts, as trying to access a file via /uc endpoint with "in this group" permission while being only logged in to a G Suite account has no authorization issues.