Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Production deploy #2883

Merged
merged 7 commits into from
Mar 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions doc/how-to/how-to-grant-metabase-permissions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# How to grant Metabase permissions

## What is Metabase?
[Metabase](https://www.metabase.com/) is an open source BI service which we self-host as part of PlanX. It allows teams to view and self-serve analytics dashboards related to their flows, applications, and users.

Metabase is set up and running on both Staging and Production environments, but only the Production instance (with Production data) has dashboards maintained and curated for teams.

## Context
Metabase accesses our staging and production databases through the `metabase_read_only` role which has `SELECT` (read-only) access to a subset of tables and views. This ensures that sensitive user data cannot be inadvertently exposed via Metabase, and any new tables added to the PlanX database have to be explicitly exposed via this role.

The permissions granted for the `metabase_read_only` role are applied via Hasura migrations. This ensures that we have a version-controlled history of this role, and it's access level is documented in code. This is not controlled via Pulumi (IaC) as this is not used for local development or test environments ("Pizzas"), which would necessitate a second method for these environments.

## How is this role used?

### Staging & Production
The `metabase_read_only` role is granted to the `metabase_user` database user, which is manually set up on both staging and production with the following SQL -

```sql
CREATE USER metabase_user WITH PASSWORD `$PASSWORD`;
GRANT metabase_read_only TO metabase_user;
```

The password for Staging and Production databases can be found the OSL 1Password account.

The username and password for Metabase are not controlled via IaC - they are manually entered via the Metabase "Admin" dashboard (`Admin Setting > Databases > "staging" | "production" > Username / Password fields`).

Please note - this is separate to the role used to read/write Metabase internal application data (such as dashboard and queries). This role is setup in IaC [here](https://github.com/theopensystemslab/planx-new/blob/main/infrastructure/application/index.ts#L100). For more information, please see [the Metabase docs](https://www.metabase.com/docs/latest/installation-and-operation/configuring-application-database).

### Locally & Pizzas
If you wish to run Metabase locally using the "analytics" Docker profile (`pnpm analytics` from project root), you will need to manually run the above SQL on your local database with a password of your choice. Alternatively, you can use the root DB username/password.

The Metabase service does not run on Pizzas.

## Metabase permissions
Metabase also operates it's own permissions model, which allows more fine-grained control over tables. This allows "Administrator" users access to all tables granted to the Postgres `metabase_read_only` role, but other users can only access summary views.

## Process
The process for exposing a new table / view to Metabase is as follows -

### Tables
Generally, we'd favour exposing views of data via Metabase. This means only certain columns can be exposed, and data can be formatted in a more user-friendly manner.

If you need to expose a new table (e.g. public data) access can be granted via a Hasura migration, e.g. -

```sql
GRANT SELECT ON public.flows TO metabase_read_only;
```

### Views
When adding a new view, you will need to grant the `metabase_read_only` role `SELECT` access the view. Access should be applied via Hasura migrations, e.g. -

```sql
GRANT SELECT ON public.YOUR_NEW_VIEW TO metabase_read_only;
```
36 changes: 9 additions & 27 deletions editor.planx.uk/src/@planx/components/Confirmation/Public.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import Check from "@mui/icons-material/Check";
import Box from "@mui/material/Box";
import { styled } from "@mui/material/styles";
import Typography from "@mui/material/Typography";
import { QuestionAndResponses } from "@opensystemslab/planx-core/types";
import Card from "@planx/components/shared/Preview/Card";
import { SummaryListTable } from "@planx/components/shared/Preview/SummaryList";
import { PublicProps } from "@planx/components/ui";
import { useStore } from "pages/FlowEditor/lib/store";
import React, { useEffect, useState } from "react";
Expand All @@ -14,20 +14,6 @@ import ReactMarkdownOrHtml from "ui/shared/ReactMarkdownOrHtml";

import type { Confirmation } from "./model";

const Table = styled("table")(({ theme }) => ({
width: "100%",
borderCollapse: "collapse",
"& tr": {
borderBottom: `1px solid ${theme.palette.grey[400]}`,
"&:last-of-type": {
border: "none",
},
"& td": {
padding: theme.spacing(1.5, 1),
},
},
}));

export type Props = PublicProps<Confirmation>;

export default function ConfirmationComponent(props: Props) {
Expand Down Expand Up @@ -70,18 +56,14 @@ export default function ConfirmationComponent(props: Props) {
</Banner>
<Card>
{props.details && (
<Table>
<tbody>
{Object.entries(props.details).map((item, i) => (
<tr key={i}>
<td>{item[0]}</td>
<td>
<b>{item[1]}</b>
</td>
</tr>
))}
</tbody>
</Table>
<SummaryListTable>
{Object.entries(props.details).map((item) => (
<>
<Box component="dt">{item[0]}</Box>
<Box component="dd">{item[1]}</Box>
</>
))}
</SummaryListTable>
)}

{<FileDownload data={data} filename={sessionId || "application"} />}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Typography from "@mui/material/Typography";
import { visuallyHidden } from "@mui/utils";
import Card from "@planx/components/shared/Preview/Card";
import QuestionHeader from "@planx/components/shared/Preview/QuestionHeader";
import { SummaryListTable } from "@planx/components/shared/Preview/SummaryList";
import type { PublicProps } from "@planx/components/ui";
import { Feature } from "@turf/helpers";
import { useFormik } from "formik";
Expand All @@ -15,7 +16,6 @@ import { useAnalyticsTracking } from "pages/FlowEditor/lib/analyticsProvider";
import { useStore } from "pages/FlowEditor/lib/store";
import { handleSubmit } from "pages/Preview/Node";
import React from "react";
import { FONT_WEIGHT_SEMI_BOLD } from "theme";

import type { SiteAddress } from "../FindProperty/model";
import { FETCH_BLPU_CODES } from "../FindProperty/Public";
Expand Down Expand Up @@ -201,41 +201,10 @@ interface PropertyDetail {
interface PropertyDetailsProps {
data: PropertyDetail[];
showPropertyTypeOverride?: boolean;
showChangeButton?: boolean;
overrideAnswer: (fn: string) => void;
}

// Borrows and tweaks grid style from Review page's `SummaryList`
const PropertyDetailsList = styled(Box)(({ theme }) => ({
display: "grid",
gridTemplateColumns: "1fr 2fr 100px",
marginTop: theme.spacing(2),
marginBottom: theme.spacing(2),
"& > *": {
borderBottom: `1px solid ${theme.palette.border.main}`,
paddingBottom: theme.spacing(1.5),
paddingTop: theme.spacing(1.5),
verticalAlign: "top",
margin: 0,
},
"& ul": {
listStylePosition: "inside",
padding: 0,
margin: 0,
},
"& dt": {
// left column
fontWeight: FONT_WEIGHT_SEMI_BOLD,
},
"& dd:nth-of-type(n)": {
// middle column
paddingLeft: "10px",
},
"& dd:nth-of-type(2n)": {
// right column
textAlign: "right",
},
}));

function PropertyDetails(props: PropertyDetailsProps) {
const { data, showPropertyTypeOverride, overrideAnswer } = props;
const filteredData = data.filter((d) => Boolean(d.detail));
Expand All @@ -248,7 +217,7 @@ function PropertyDetails(props: PropertyDetailsProps) {
};

return (
<PropertyDetailsList component="dl">
<SummaryListTable showChangeButton={true}>
{filteredData.map(({ heading, detail, fn }: PropertyDetail) => (
<React.Fragment key={heading}>
<Box component="dt">{heading}</Box>
Expand All @@ -275,6 +244,6 @@ function PropertyDetails(props: PropertyDetailsProps) {
</Box>
</React.Fragment>
))}
</PropertyDetailsList>
</SummaryListTable>
);
}
Loading
Loading