S3 Viewer logo

Backstage S3 Viewer Plugin

Created by Spreadgroup

S3 Viewer adds an S3 browser to your Backstage portal. It gives you a page to explore buckets and objects in a simple table. You can view object details. You can preview small images. You can download files when allowed. Requests go through your Backstage backend so auth and permissions stay in place.

The plugin fits day to day work. Use it to inspect build artifacts, logs, static docs, and media stored in S3. Open a bucket and click through folders like a file explorer. Copy a deep link to share a specific path or object with a teammate. Keep focus in one place instead of jumping to external consoles.

Under the hood it ships a frontend and a backend. The backend exposes endpoints to list buckets, fetch metadata, preview content, and stream or download objects. The UI reads from those endpoints and renders a clean view of what is inside S3. It also supports generating shareable URLs for the exact item you are viewing. These choices make the plugin useful for support work, incident triage, and reviews during delivery.

S3 Viewer is listed in the Backstage plugin marketplace as a way to visualize S3 buckets and their contents in file explorer style. The code lives in a public GitHub repo and sees active updates.

Installation Instructions

These instructions apply to self-hosted Backstage only.

Install the frontend package

Copy
# from your Backstage root
yarn --cwd packages/app add @spreadshirt/backstage-plugin-s3-viewer

Add the page route

Copy
// packages/app/src/App.tsx
import { S3ViewerPage } from '@spreadshirt/backstage-plugin-s3-viewer';

// inside your <FlatRoutes> in the returned App component
// other routes
<Route path="/s3-viewer" element={<S3ViewerPage />} />
Copy
// packages/app/src/components/Root/Root.tsx
import { SidebarItem } from '@backstage/core-components';
import { SiAmazons3 } from 'react-icons/si';

// inside <SidebarPage>
<SidebarItem icon={SiAmazons3} to="s3-viewer" text="S3 Viewer" />

Install the backend package new backend system

Copy
# from your Backstage root
yarn --cwd packages/backend add @spreadshirt/backstage-plugin-s3-viewer-backend

Register the backend plugin new backend system

Copy
// packages/backend/src/index.ts
import { createBackend } from '@backstage/backend-defaults';

const backend = createBackend();

// other backend plugins
backend.add(import('@spreadshirt/backstage-plugin-s3-viewer-backend'));

backend.start();

Configure S3 connections

Add this to your app config. Adjust names endpoints regions and secrets for your setup.

Copy
# app-config.yaml
s3:
  bucketLocatorMethods:
    - type: config
      platforms:
        - endpoint: http://endpoint-one.com
          name: endpoint-one-name
          region: us-east-1
          accessKeyId: ${ENDPOINT_ONE_ACCESS_KEY}
          secretAccessKey: ${ENDPOINT_ONE_SECRET_KEY}
        - endpoint: http://endpoint-two.com
          name: endpoint-two-name
          region: us-east-1
          accessKeyId: ${ENDPOINT_TWO_ACCESS_KEY}
          secretAccessKey: ${ENDPOINT_TWO_SECRET_KEY}
    - type: radosgw-admin
      platforms:
        - endpoint: http://radosgw-endpoint.com
          name: radosgw-endpoint-name
          region: us-east-1
          accessKeyId: ${RADOSGW_ACCESS_KEY}
          secretAccessKey: ${RADOSGW_SECRET_KEY}
    - type: iam-role
      platforms:
        - endpoint: http://iam-endpoint.com
          name: iam-endpoint-name
          region: us-east-1
  allowedBuckets:
    - platform: endpoint-one-name
      buckets:
        - allowed-bucket-one
        - allowed-bucket-two
    - platform: radosgw-endpoint-name
      buckets:
        - other-allowed-bucket
    - platform: iam-endpoint-name
      buckets:
        - another-bucket-name
  bucketRefreshSchedule:
    frequency: { minutes: 30 }
    timeout: { minutes: 1 }

The UI fetches images and downloads through the browser. Set the cookie after sign in so those requests are authorized.

Copy
// packages/app/src/App.tsx
import React from 'react';
import { createApp } from '@backstage/app-defaults';
import { SignInPage } from '@backstage/core-components';
import { useApi, IdentityApi } from '@backstage/core-plugin-api';
import { S3ApiRef } from '@spreadshirt/backstage-plugin-s3-viewer';

const app = createApp({
  components: {
    SignInPage: props => {
      const s3ViewerApi = useApi(S3ApiRef);
      return (
        <SignInPage
          {...props}
          provider={{
            id: 'guest',
            title: 'Guest',
            message: 'Login as a guest',
          }}
          onSignInSuccess={async (identityApi: IdentityApi) => {
            props.onSignInSuccess(identityApi);
            await s3ViewerApi.setCookie();
          }}
        />
      );
    },
  },
});

export default app;

No extra backend code is needed for this step when using the new backend system. The backend plugin provides the endpoint used by setCookie.

Add a permission policy for S3 viewer

Decide what users can do. Here is a simple example that allows listing but denies object download. Wire this in your permission backend as you do for other policies.

Copy
// packages/backend/src/plugins/permission.ts
import {
  isPermission,
  isResourcePermission,
  type PolicyDecision,
  type PolicyQuery,
  AuthorizeResult,
  type PermissionPolicy,
} from '@backstage/plugin-permission-node';
import {
  S3_VIEWER_RESOURCE_TYPE,
  s3ViewerPermissions,
} from '@spreadshirt/backstage-plugin-s3-viewer-common';
import { type BackstageIdentityResponse } from '@backstage/plugin-auth-node';

export class CustomPolicy implements PermissionPolicy {
  async handle(
    request: PolicyQuery,
    _user?: BackstageIdentityResponse,
  ): Promise<PolicyDecision> {
    if (isResourcePermission(request.permission, S3_VIEWER_RESOURCE_TYPE)) {
      if (isPermission(request.permission, s3ViewerPermissions.s3ObjectDownload)) {
        return { result: AuthorizeResult.DENY };
      }
      return { result: AuthorizeResult.ALLOW };
    }
    return { result: AuthorizeResult.ALLOW };
  }
}

You can also use conditional decisions to filter visible buckets. Use helpers from the common package if you need that.

Optional refresh API setup

You can trigger a bucket refresh on demand with a service token.

Static token example

Copy
# app-config.yaml
backend:
  auth:
    externalAccess:
      - type: static
        options:
          token: ${S3_API_REFRESH_TOKEN}
          subject: s3-viewier-api-test-token
        accessRestrictions:
          - plugin: s3-viewer

Plugin to plugin token example

Copy
// inside a backend module
const { token } = await auth.getPluginRequestToken({
  onBehalfOf: await auth.getOwnServiceCredentials(),
  targetPluginId: 's3-viewer',
});

// call POST /api/s3-viewer/buckets/refresh with Authorization Bearer <token>

About the old backend system

Current versions of this plugin support the new backend system only. If your app still runs the old backend system, migrate to the new backend system before installing this plugin.

Changelog

This changelog is produced from commits made to the S3 Viewer plugin since 3 months ago, and based on the code located here. It may not contain information about all commits. Releases and version bumps are intentionally omitted. This changelog is generated by AI.

Breaking changes

  • Change the stream route format to support relative refs in html. The endpoint now sits in the path. The key can include escaped and plain slashes. This can break callers that use the old stream path. #168 merged 3 months ago

Features

  • Allow viewing html files in the browser. Stop sending attachment for html. #158 merged 3 months ago
  • Add an API to refresh buckets on demand with a service token. Useful for service to service calls. #160 merged 3 months ago
  • Improve paging. Use a fixed total so you can move past the second page and jump to the last page. #167 merged 3 months ago
  • Support relative refs in html when streaming objects. #168 merged 3 months ago

Bug fixes

  • Respect object content encoding metadata. Browsers now render gzipped html correctly. #163 merged 3 months ago
  • Fix html content type check logic. #165 merged 3 months ago
  • Do not force the download attribute on links. Rely on the presence of a response header instead. #162 merged 3 months ago

Documentation

  • Add docs for the new s3 api. Update changeset notes. #164 merged 3 months ago

Maintenance

  • Bump Backstage dependencies to version 1.43.2. No code changes in the plugin. #177 merged 2 weeks ago
  • Bump Backstage to the latest. Align other packages with upstream. Replace momentjs with luxon. Address security issues. #175 merged 1 month ago
  • Bump to the latest Backstage. Address security alerts. Update the test backend plugin to match the new major. #171 merged 2 months ago

Set up Backstage in minutes with Roadie