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

Load rule sets into the replicator #20

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
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
7 changes: 6 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ WORKDIR /replicator
COPY package.json .
COPY package-lock.json .
COPY tsconfig.json .
COPY index.ts .
COPY *.ts .
RUN npm ci
RUN npm run build

Expand Down Expand Up @@ -34,4 +34,9 @@ COPY --from=build /replicator .

COPY start.sh /usr/local/bin/start.sh

RUN mkdir -p /var/lib/replicator/policies
VOLUME /var/lib/replicator/policies

ENV JINAGA_POLICIES /var/lib/replicator/policies

ENTRYPOINT [ "start.sh" ]
100 changes: 99 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,104 @@ Run:
docker run -it --rm -p8080:8080 jinaga/jinaga-replicator
```

If you have policy files that you want to use with this image, you can mount a directory containing your policy files to the container's `/var/lib/replicator/policies` directory:

```bash
docker run -it --rm -p8080:8080 -v /path/to/your/policies:/var/lib/replicator/policies jinaga/jinaga-replicator
```

Replace `/path/to/your/policies` with the path to the directory on your host machine that contains your policy files.

### Using as a Base Image

To use this image as a base image and copy your policy files into the `/var/lib/replicator/policies` directory, create a `Dockerfile` like this:

```dockerfile
FROM jinaga/jinaga-replicator

# Copy policy files into the /var/lib/replicator/policies directory
COPY *.policy /var/lib/replicator/policies/

# Ensure the policy files have the correct permissions
RUN chmod -R 755 /var/lib/replicator/policies
```

Build the new Docker image:

```bash
docker build -t my-replicator-with-policies .
```

This will create a new Docker image named `my-replicator-with-policies` with the policy files included.

## Security Policies

Policies determine who is authorized to write facts, and to whom to distribute facts.
They also determine the conditions under which to purge facts.
Authorization, distribution, and purge rules are defined in policy files.

### Policy Files

Policy files have three sections:

- **authorization**: Who is authorized to write facts.
- **distribution**: To whom to distribute facts.
- **purge**: Conditions under which to purge facts.

Here is an example policy file:

```
authorization {
(post: Blog.Post) {
creator: Jinaga.User [
creator = post->site: Blog.Site->creator: Jinaga.User
]
} => creator
(deleted: Blog.Post.Deleted) {
creator: Jinaga.User [
creator = deleted->post: Blog.Post->site: Blog.Site->creator: Jinaga.User
]
} => creator
}
distribution {
share (user: Jinaga.User) {
name: Blog.User.Name [
name->user: Jinaga.User = user
!E {
next: Blog.User.Name [
next->prior: Blog.User.Name = name
]
}
]
} => name
with (user: Jinaga.User) {
self: Jinaga.User [
self = user
]
} => self
}
purge {
(post: Blog.Post) {
deleted: Blog.Post.Deleted [
deleted->post: Blog.Post = post
]
} => deleted
}
```

You can produce a policy file from .NET using the `dotnet jinaga` command line tool, or from JavaScript using the `jinaga` package.

### No Security Policies

To run a replicator with no security policies, create an empty `no-security-policies` file in the policy directory.

```bash
touch /var/lib/replicator/policies/no-security-policies
```

This file opts-in to running the replicator with no security policies.
If the file is not present, the replicator will exit with an error message indicating that no security policies are found.

## Release

To release a new version of Jinaga replicator, bump the version number, push the tag, and let GitHub Actions do the rest.
Expand All @@ -47,4 +145,4 @@ git checkout main
git pull
npm version patch
git push --follow-tags
```
```
33 changes: 23 additions & 10 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import express = require("express");
import * as http from "http";
import { JinagaServer } from "jinaga-server";
import process = require("process");
import { loadPolicies } from "./loadPolicies";

process.on('SIGINT', () => {
console.log("\n\nStopping replicator\n");
Expand All @@ -15,15 +16,27 @@ app.set('port', process.env.PORT || 8080);
app.use(express.json());
app.use(express.text());

const pgConnection = process.env.JINAGA_POSTGRESQL ||
'postgresql://repl:replpw@localhost:5432/replicator';
const { handler } = JinagaServer.create({
pgStore: pgConnection
});
async function initializeReplicator() {
const pgConnection = process.env.JINAGA_POSTGRESQL ||
'postgresql://repl:replpw@localhost:5432/replicator';
const policiesPath = process.env.JINAGA_POLICIES || 'policies';
const ruleSet = await loadPolicies(policiesPath);
const { handler } = JinagaServer.create({
pgStore: pgConnection,
authorization: ruleSet ? a => ruleSet.authorizationRules : undefined,
distribution: ruleSet ? d => ruleSet.distributionRules : undefined,
purgeConditions: ruleSet ? p => ruleSet.purgeConditions : undefined
});

app.use('/jinaga', handler);
app.use('/jinaga', handler);

server.listen(app.get('port'), () => {
console.log(` Replicator is running at http://localhost:${app.get('port')} in ${app.get('env')} mode`);
console.log(' Press CTRL-C to stop\n');
});
server.listen(app.get('port'), () => {
console.log(` Replicator is running at http://localhost:${app.get('port')} in ${app.get('env')} mode`);
console.log(' Press CTRL-C to stop\n');
});
}

initializeReplicator()
.catch((error) => {
console.error("Error initializing replicator.", error);
});
78 changes: 78 additions & 0 deletions loadPolicies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { Dirent, readFile, readdir } from "fs";
import { RuleSet } from "jinaga";
import { join } from "path";

const MARKER_FILE_NAME = "no-security-policies";

export async function loadPolicies(path: string): Promise<RuleSet | undefined> {
const { policyFiles, hasMarkerFile } = await findPolicyFiles(path);

if (hasMarkerFile && policyFiles.length > 0) {
throw new Error(`Security policies are disabled, but there are policy files in ${path}.`);
}

if (!hasMarkerFile && policyFiles.length === 0) {
throw new Error(`No security policies found in ${path}.`);
}

if (policyFiles.length === 0) {
// Leave the replicator wide open
return undefined;
}

let ruleSet = RuleSet.empty;

for (const fileName of policyFiles) {
const rules = await loadRuleSetFromFile(fileName);
ruleSet = ruleSet.merge(rules);
}

return ruleSet;
}

async function findPolicyFiles(dir: string): Promise<{ policyFiles: string[], hasMarkerFile: boolean }> {
const policyFiles: string[] = [];
let hasMarkerFile = false;

const entries = await new Promise<Dirent[]>((resolve, reject) => {
readdir(dir, { withFileTypes: true }, (err, files) => {
if (err) {
reject(err);
} else {
resolve(files);
}
});
});

for (const entry of entries) {
const fullPath = join(dir, entry.name);
if (entry.isDirectory()) {
const result = await findPolicyFiles(fullPath);
policyFiles.push(...result.policyFiles);
hasMarkerFile = hasMarkerFile || result.hasMarkerFile;
} else if (entry.isFile()) {
if (entry.name.endsWith('.policy')) {
policyFiles.push(fullPath);
} else if (entry.name === MARKER_FILE_NAME) {
hasMarkerFile = true;
}
}
}

return { policyFiles, hasMarkerFile };
}

async function loadRuleSetFromFile(path: string): Promise<RuleSet> {
const description = await new Promise<string>((resolve, reject) => {
readFile(path, 'utf8', (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});

const ruleSet = RuleSet.loadFromDescription(description);
return ruleSet;
}
26 changes: 15 additions & 11 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"license": "MIT",
"dependencies": {
"express": "^4.21.1",
"jinaga": "^6.3.0",
"jinaga": "^6.5.2",
"jinaga-server": "^3.2.0",
"pg": "^8.13.0"
},
Expand Down