Building a Note/Thought Sharing WebApp with OPAL Authorization and Custom MySQL Fetcher
Published: 2024-06-20 10:37:06

Integrating authorization using OPA (Open Policy Agent) with a custom MySQL fetcher in a Node.js Express application, and consuming it with a React frontend, involves several steps. Here’s a detailed walkthrough that covers setting up the backend, Docker environment, and frontend interactions. Below, I'll structure the blog and include all the necessary components you requested.

---

### Goal: Implementing Authorization with OPA and Custom MySQL Fetcher

In this tutorial, we'll set up a Node.js Express backend with JWT authentication and OPA for authorization, utilizing a custom MySQL fetcher. Our frontend, built with React, will interact with this backend to perform CRUD operations on notes.

#### Prerequisites

Before we begin, make sure you have:

- Node.js and npm installed
- Docker installed (for local development environment setup)
- Basic knowledge of React and Node.js

#### 1. Setting up the Backend

##### Docker Compose Configuration (`docker-compose.yml`)

```yaml
version: "3.8"
services:
  broadcast_channel:
    image: postgres
    environment:
      - POSTGRES_DB=postgres
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres

  adminer:
    image: adminer
    restart: always
    ports:
      - 8083:8080

  example_db:
    image: mysql:8.0
    cap_add:
      - SYS_NICE
    restart: always
    environment:
      - MYSQL_DATABASE=test
      - MYSQL_USER=test
      - MYSQL_PASSWORD=mysql
      - MYSQL_ROOT_PASSWORD=mysql
    logging:
      options:
        max-size: 10m
        max-file: "3"
    ports:
      - "3306:3306"
    volumes:
      - ./sql/init.sql:/docker-entrypoint-initdb.d/init.sql

  opal_server:
    image: permitio/opal-server
    environment:
      - OPAL_BROADCAST_URI=postgres://postgres@broadcast_channel:5432/postgres
      - UVICORN_NUM_WORKERS=4
      - OPAL_POLICY_REPO_URL=https://github.com/sonu2164/opal-example-policy-repo
      - OPAL_POLICY_REPO_POLLING_INTERVAL=30
      - OPAL_DATA_CONFIG_SOURCES={"config":{"entries":[{"url":"mysql://root@example_db:3306/test?password=mysql","config":{"fetcher":"MySQLFetchProvider","query":"SELECT * FROM users","connection_params":{"host":"example_db","user":"root","port":3306,"db":"test","password":"mysql"}},"topics":["mysql"],"dst_path":"users"},{"url":"mysql://root@example_db:3306/test?password=mysql","config":{"fetcher":"MySQLFetchProvider","query":"SELECT * FROM notes","connection_params":{"host":"example_db","user":"root","port":3306,"db":"test","password":"mysql"}},"topics":["mysql"],"dst_path":"notes"}]}}
    ports:
      - "7002:7002"
    depends_on:
      - broadcast_channel

  opal_client:
    build:
      context: .
    environment:
      - OPAL_SERVER_URL=http://opal_server:7002
      - OPAL_LOG_FORMAT_INCLUDE_PID=true
      - OPAL_FETCH_PROVIDER_MODULES=opal_common.fetcher.providers,opal_fetcher_mysql.provider
      - OPAL_INLINE_OPA_LOG_FORMAT=http
      - OPA_LOG_LEVEL=debug
      - OPAL_SHOULD_REPORT_ON_DATA_UPDATES=True
      - OPAL_DATA_TOPICS=mysql
    ports:
      - "7766:7000"
      - "8181:8181"
    depends_on:
      - opal_server
      - example_db
    command: sh -c "./wait-for.sh opal_server:7002 example_db:3306 --timeout=20 -- ./start.sh"
```

**Explanation:**
- Sets up PostgreSQL and MySQL databases for storage (`broadcast_channel` and `example_db`).
- Initializes Opal Server (`opal_server`) with configurations to connect to PostgreSQL and MySQL databases.
- Deploys Opal Client (`opal_client`) and connects it to Opal Server and MySQL database.

#### 2. Backend Application (`server/index.js`)

```javascript
const express = require("express");
const axios = require("axios");
const cors = require("cors");
const updateOpaData = require("./src/updata_opa_data").updateOpaData;
const noteshandler = require("./src/noteshandler");
const userhandler = require("./src/userhandler");
const app = express();
const port = 3001;
const { PrismaClient } = require("@prisma/client");
const prisma = new PrismaClient();

const jwt = require('jsonwebtoken');

app.use(express.json());
app.use(cors());

// Authentication middleware
const authenticate = async (req, res, next) => {
  const authHeader = req.headers.authorization;

  if (!authHeader) {
    return res.status(401).json({ error: 'Authorization header missing' });
  }

  const token = authHeader.split(' ')[1]; // Bearer <token>

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    const user = await prisma.user.findUnique({
      where: {
        id: decoded.userId,
      },
    });

    if (!user) {
      return res.status(401).json({ error: 'User not found' });
    }

    req.user = user;
    next();
  } catch (error) {
    console.error('JWT Verification Error:', error);
    return res.status(401).json({ error: 'Invalid or expired token' });
  }
};

// Authorization middleware
const authorization = async (req, res, next) => {
  try {
    const userId = parseInt(req.user.id);
    const noteId = parseInt(req.body.noteId);

    const check = await axios.post(
      "http://localhost:8181/v1/data/app/rbac/allow",
      {
        input: {
          user: userId,
          note: noteId,
        },
      }
    );

    if (check.data.result) {
      next(); // Proceed to the next middleware or route handler
    } else {
      res.status(403).json({ message: "Unauthorized" });
    }
  } catch (error) {
    console.error("Error during authorization check:", error);
    res.status(500).json({ message: "Internal Server Error" });
  }
};

// Routes
app.get("/", (req, res) => {
  res.send("Hello World!");
});

app.post("/user/login", async (req, res) => {
  const { email, password } = req.body;
  const user = await userhandler.loginUser(email, password);
  return res.status(200).json(user);
});

app.post("/user/register", async (req, res) => {
  const { name, email, password, role } = req.body
  const user = await userhandler.registerUser(name, email, password, role);
  await updateOpaData();
  return res.status(200).json(user);
});

app.get("/notes", async (req, res) => {
  const notes = await noteshandler.notes();
  return res.status(200).json(notes);
});

app.post("/newNote", authenticate, async (req, res) => {
  const noteObj = {
    title: req.body.title,
    description: req.body.description,
    user: { connect: { id: req.user.id } },
  };

  try {
    const note = await noteshandler.newNote(noteObj);
    await updateOpaData(); // Update OPA data after creating a new note
    return res.status(200).json(note);
  } catch (error) {
    console.error("Error creating new note:", error);
    return res.status(500).json({ message: "Internal Server Error" });
  }
});

app.post("/note/update", authenticate, authorization, async (req, res) => {
  const { noteId, title, description } = req.body;

  try {
    const note = await noteshandler.updateNote(noteId, title, description);
    await updateOpaData(); // Update OPA data after updating a note
    return res.status(200).json(note);
  } catch (error) {
    console.error("Error updating note:", error);
    return res.status(500).json({ message: "Internal Server Error" });
  }
});

app.post("/note/delete", authenticate, authorization, async (req, res) => {
  const { noteId } = req.body;
  try {
    const delRes = await noteshandler.deleteNote(noteId);
    await updateOpaData(); // Update OPA data after deleting a note
    return res.status(200).json({ status: delRes });
  } catch (error) {
    console.error("Error deleting note:", error);
    return res.status(500).json({ message: "Internal Server Error" });
  }
});

app.listen(port, () => {
  console.log(`App server listening on port ${port}`);
});
```

**Explanation:**
- Sets up Express server with middleware for JSON parsing, CORS handling, JWT authentication, and authorization.
- Defines routes for user authentication (`/user/login`, `/user/register`), note CRUD operations (`/notes`, `/newNote`, `/note/update`, `/note/delete`).
- Uses JWT tokens for authentication and checks authorization using OPA.
- Utilizes Axios to make HTTP requests to OPA server (`opal_server`) for authorization checks.

#### 3. Frontend Integration (React)

To interact with these backend endpoints from a React frontend, you can use Axios or Fetch API for making HTTP requests. Here's a basic example:

### Check the endpoints using postman

#Head over to github repo for reference 

https://github.com/sonu2164/keeper_opal_auth_quira_submission
**See readme in repo to run the project **