Skip to content

Commit

Permalink
#448: processed feedback
Browse files Browse the repository at this point in the history
  • Loading branch information
basmasking committed Jan 26, 2024
1 parent a4f7bec commit 66f8053
Show file tree
Hide file tree
Showing 18 changed files with 120 additions and 81 deletions.
14 changes: 11 additions & 3 deletions documentation/docs/deploy/segmentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,12 @@ Imports have multiple properties that can be configured. These properties will b

### Trusted clients

When building a distributed application, you don't want all functions to be available by the outside world. Some functions are only used internally by other segments. To protect the access to these functions, Jitar provides a `secret` property in the [runtime services](../fundamentals/runtime-services#node). This secret is used to create trusted clients. Trusted clients can access functions with the `protected` access level.
When building a distributed application, you don't want all functions to be available by the outside world. Some functions are only used internally by other segments. To protect the access to these functions, Jitar provides a `trustKey` property in the [runtime services](../fundamentals/runtime-services#node). This key is used to create trusted clients. Trusted clients can access functions with the `protected` access level.

Any client that wants to access a protected function must provide a valid access key. The access key needs to be added to the http header `x-access-key`. Any node that has a valid access key is considered a trusted client, and automatically adds the access key to the http header of outgoing requests. Any node that doesn't have a valid access key is considered an untrusted client and can only access `public` functions.
Any client that wants to access a protected function must provide a valid key. It needs to be added to the http header `X-Jitar-Protected-Access-Key`. Any node that has a valid key is automatically considered a trusted client, and adds the access key to the http header of outgoing requests. Any node that doesn't have a valid access key is considered an untrusted client and can only access `public` functions.

::: info Note
To enable trusted clients, the gateway must always have a shared secret configured. Any node that wants to register itself with a secret, must have the same secret in its configuration.
To enable trusted clients, the gateway must always have a trusted key configured. Any node that wants to register itself as a trusted client, must have the same value for the `trustKey` in its configuration.
:::

### Access protection
Expand Down Expand Up @@ -115,6 +115,14 @@ Functions that need to be accessible from outside need to have the public access
To protect the access to public functions [authentication and authorization](../develop/security.md#authentication-and-authorization) needs be applied.
:::

::: warning NOTE
Any function is considered `public` if one of the implementations is public. This means that a function with multiple versions can be public, even if one of the versions is private or protected.
:::

::: warning NOTE
Any function is considered `protected` if one of the implementations is protected. This means that a function with multiple versions can be protected, even if one of the versions is private.
:::

### Versioning

Jitar generates an endpoint for each public function. These endpoints are used for automating the internal communication, but can also be used by external applications with [our RPC API](../integrate/rpc-api). To control the implementation of breaking changes in external applications, Jitar supports providing multiple versions of functions.
Expand Down
12 changes: 6 additions & 6 deletions documentation/docs/fundamentals/runtime-services.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ The following configuration properties are available:
* repository - url of the repository (required).
* segments - list of segment names to load (optional, loads all segments by default).
* middlewares - list of [middleware modules](../develop/middleware.md) to load (optional).
* secret - shared secret for creating trusted clients (optional).
* trustKey - key for creating trusted client (optional).

A full configuration example looks like this:

Expand All @@ -128,7 +128,7 @@ A full configuration example looks like this:
"repository": "http://repository.example.com:3000",
"segments": ["segment1", "segment2"],
"middlewares": ["./middleware1", "./middleware2"],
"secret": "MY_SHARED_SECRET"
"trustKey": "MY_TRUST_KEY"
}
}
```
Expand Down Expand Up @@ -164,7 +164,7 @@ The following configuration properties are available:
* repository - url of the repository (required).
* monitor - node monitoring interval in milliseconds (optional, default `5000`).
* middlewares - list of [middleware modules](../develop/middleware.md) to load (optional).
* secret - shared secret for creating trusted clients (optional).
* trustKey - key for creating trusted clients (optional).

A full configuration example looks like this:

Expand All @@ -176,7 +176,7 @@ A full configuration example looks like this:
"repository": "http://repository.example.com:3000",
"monitor": 5000,
"middlewares": ["./middleware1", "./middleware2"],
"secret": "MY_SHARED_SECRET"
"trustKey": "MY_TRUST_KEY"
}
}
```
Expand Down Expand Up @@ -237,7 +237,7 @@ The standalone service has the same configuration properties as the repository s
* assets - list of whitelisted assets (optional, default `undefined`).
* middlewares - list of [middleware modules](../develop/middleware.md) to load (optional).
* overrides - map with import overrides (optional, default `undefined`).
* secret - shared secret for creating trusted clients (optional).
* trustKey - key for creating trusted clients (optional).

A full configuration example looks like this:

Expand All @@ -252,7 +252,7 @@ A full configuration example looks like this:
"assets": ["*.html", "*.js", "*.css", "assets/**/*"],
"middlewares": ["./middleware1", "./middleware2"],
"overrides": { "./my-module": "./alternative-module" },
"secret": "MY_SHARED_SECRET"
"trustKey": "MY_TRUST_KEY"
}
}
```
Expand Down
2 changes: 1 addition & 1 deletion examples/concepts/access-protection/requests.http
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ GET http://localhost:3000/rpc/web/guessSecret?secret=123

// Run the protected function with key (succeeds)
GET http://localhost:3000/rpc/game/checkSecret?secret=123 HTTP/1.1
x-access-key: VERY_SECRET_KEY
X-Jitar-Protected-Access-Key: VERY_SECRET_KEY

###

Expand Down
12 changes: 6 additions & 6 deletions packages/runtime/src/RuntimeBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,28 +121,28 @@ export default class RuntimeBuilder
return repository;
}

buildGateway(secret?: string): LocalGateway
buildGateway(trustKey?: string): LocalGateway
{
if (this.#repository === undefined)
{
throw new RuntimeNotBuilt('Repository is not set for the gateway');
}

const gateway = new LocalGateway(this.#repository, this.#url, secret);
const gateway = new LocalGateway(this.#repository, this.#url, trustKey);
gateway.healthCheckFiles = this.#healthChecks;
gateway.middlewareFiles = this.#middlewares;

return gateway;
}

buildNode(secret?: string): LocalNode
buildNode(trustKey?: string): LocalNode
{
if (this.#repository === undefined)
{
throw new RuntimeNotBuilt('Repository is not set for the node');
}

const node = new LocalNode(this.#repository, this.#gateway, this.#url, secret);
const node = new LocalNode(this.#repository, this.#gateway, this.#url, trustKey);
node.segmentNames = this.#segments;
node.healthCheckFiles = this.#healthChecks;
node.middlewareFiles = this.#middlewares;
Expand Down Expand Up @@ -178,7 +178,7 @@ export default class RuntimeBuilder
return proxy;
}

buildStandalone(secret?: string): Standalone
buildStandalone(trustKey?: string): Standalone
{
if (this.#fileManager === undefined)
{
Expand All @@ -190,7 +190,7 @@ export default class RuntimeBuilder
repository.assets = this.#assets;
repository.overrides = this.#overrides;

const node = new LocalNode(repository, this.#gateway, this.#url, secret);
const node = new LocalNode(repository, this.#gateway, this.#url, trustKey);
node.segmentNames = this.#segments;

const standalone = new Standalone(repository, node, this.#url);
Expand Down
14 changes: 0 additions & 14 deletions packages/runtime/src/errors/InvalidSecret.ts

This file was deleted.

14 changes: 14 additions & 0 deletions packages/runtime/src/errors/InvalidTrustKey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@

import { Loadable } from '@jitar/serialization';

import Unauthorized from './generic/Unauthorized.js';

export default class InvalidTrustKey extends Unauthorized
{
constructor()
{
super(`Invalid trust key`);
}
}

(InvalidTrustKey as Loadable).source = 'RUNTIME_ERROR_LOCATION';
14 changes: 7 additions & 7 deletions packages/runtime/src/services/LocalGateway.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

import InvalidSecret from '../errors/InvalidSecret.js';
import InvalidTrustKey from '../errors/InvalidTrustKey.js';
import ProcedureNotFound from '../errors/ProcedureNotFound.js';

import Request from '../models/Request.js';
Expand All @@ -14,13 +14,13 @@ export default class LocalGateway extends Gateway
{
#nodes: Set<Node> = new Set();
#balancers: Map<string, NodeBalancer> = new Map();
#secret?: string;
#trustKey?: string;

constructor(repository: Repository, url?: string, secret?: string)
constructor(repository: Repository, url?: string, trustKey?: string)
{
super(repository, url);

this.#secret = secret;
this.#trustKey = trustKey;
}

get nodes()
Expand All @@ -43,11 +43,11 @@ export default class LocalGateway extends Gateway
return procedureNames.includes(fqn);
}

async addNode(node: Node, secret?: string): Promise<void>
async addNode(node: Node, trustKey?: string): Promise<void>
{
if (secret !== undefined && this.#secret !== secret)
if (trustKey !== undefined && this.#trustKey !== trustKey)
{
throw new InvalidSecret();
throw new InvalidTrustKey();
}

this.#nodes.add(node);
Expand Down
24 changes: 13 additions & 11 deletions packages/runtime/src/services/LocalNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { createNodeFilename } from '../definitions/Files.js';
import Unauthorized from '../errors/generic/Unauthorized.js';
import ImplementationNotFound from '../errors/ImplementationNotFound.js';
import ProcedureNotFound from '../errors/ProcedureNotFound.js';
import InvalidSecret from '../errors/InvalidSecret.js';
import InvalidTrustKey from '../errors/InvalidTrustKey.js';

import Procedure from '../models/Procedure.js';
import Request from '../models/Request.js';
Expand All @@ -19,27 +19,29 @@ import Repository from './Repository.js';

import { setRuntime } from '../hooks.js';

const JITAR_PROTECTED_ACCESS_HEADER_KEY = 'X-Jitar-Protected-Access-Key';

export default class LocalNode extends Node
{
#gateway?: Gateway;
#secret?: string;
#trustKey?: string;
#argumentConstructor: ArgumentConstructor;

#segmentNames: Set<string> = new Set();
#segments: Map<string, Segment> = new Map();

constructor(repository: Repository, gateway?: Gateway, url?: string, secret?: string, argumentConstructor = new ArgumentConstructor())
constructor(repository: Repository, gateway?: Gateway, url?: string, trustKey?: string, argumentConstructor = new ArgumentConstructor())
{
super(repository, url);

this.#gateway = gateway;
this.#secret = secret;
this.#trustKey = trustKey;
this.#argumentConstructor = argumentConstructor;

setRuntime(this);
}

get secret() { return this.#secret; }
get trustKey() { return this.#trustKey; }

set segmentNames(names: Set<string>)
{
Expand Down Expand Up @@ -137,25 +139,25 @@ export default class LocalNode extends Node
throw new ProcedureNotFound(request.fqn);
}

if (this.#secret !== undefined)
if (this.#trustKey !== undefined)
{
const headers = request.headers;
headers.set('x-access-key', this.#secret);
headers.set(JITAR_PROTECTED_ACCESS_HEADER_KEY, this.#trustKey);
}

return this.#gateway.run(request);
}

async #runProcedure(procedure: Procedure, request: Request): Promise<Response>
{
const secret = request.headers.get('x-access-key');
const trustKey = request.getHeader(JITAR_PROTECTED_ACCESS_HEADER_KEY);

if (secret !== undefined && this.#secret !== secret)
if (trustKey !== undefined && this.#trustKey !== trustKey)
{
throw new InvalidSecret();
throw new InvalidTrustKey();
}

if (secret === undefined && procedure.protected)
if (trustKey === undefined && procedure.protected)
{
throw new Unauthorized();
}
Expand Down
2 changes: 1 addition & 1 deletion packages/runtime/src/services/Node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ import ProcedureRuntime from './ProcedureRuntime.js';

export default abstract class Node extends ProcedureRuntime
{
get secret(): string | undefined { return undefined; }
get trustKey(): string | undefined { return undefined; }
}
2 changes: 1 addition & 1 deletion packages/runtime/src/services/Remote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export default class Remote
{
url: node.url,
procedureNames: node.getProcedureNames(),
secret: node.secret,
trustKey: node.trustKey,
};
const options =
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,16 @@ const healthGateway = new LocalGateway(REPOSITORIES.DUMMY, GATEWAY_URL);
healthGateway.addNode(NODES.GOOD);
healthGateway.addNode(NODES.BAD);

const protectedGateway = new LocalGateway(REPOSITORIES.DUMMY, GATEWAY_URL, 'MY_PROTECTED_ACCESS_KEY');
protectedGateway.addNode(NODES.FIRST, 'MY_PROTECTED_ACCESS_KEY');
protectedGateway.addNode(NODES.SECOND);

const GATEWAYS =
{
STANDALONE: standaloneGateway,
DISTRIBUTED: distributedGateway,
HEALTH: healthGateway
HEALTH: healthGateway,
PROTECTED: protectedGateway
};

export { GATEWAYS, GATEWAY_URL };
24 changes: 24 additions & 0 deletions packages/runtime/test/services/LocalGateway.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { describe, expect, it } from 'vitest';

import ProcedureNotFound from '../../src/errors/ProcedureNotFound';
import InvalidSecret from '../../src/errors/InvalidSecret';
import Request from '../../src/models/Request';
import Version from '../../src/models/Version';

Expand Down Expand Up @@ -81,4 +82,27 @@ describe('services/LocalGateway', () =>
expect(run).rejects.toEqual(new ProcedureNotFound('nonExisting'));
});
});

describe('.addNode(node, accessKey)', () =>
{
it('should not add a node with an incorrect access key', async () =>
{
const node = gateway.nodes[0];
const protectedGateway = GATEWAYS.PROTECTED;

const addNode = async () => protectedGateway.addNode(node, 'INCORRECT_ACCESS_KEY');

expect(addNode).rejects.toEqual(new InvalidSecret());
});

it('should not add a node with an access key to an unprotected gateway', async () =>
{
const node = gateway.nodes[0];
const unprotectedGateway = GATEWAYS.STANDALONE;

const addNode = async () => unprotectedGateway.addNode(node, 'NODE_ACCESS_KEY');

expect(addNode).rejects.toEqual(new InvalidSecret());
});
});
});
12 changes: 6 additions & 6 deletions packages/server-nodejs/src/configuration/GatewayConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,29 @@ export const gatewaySchema = z
repository: z.string().url(),
middlewares: z.array(z.string()).optional(),
monitor: z.number().optional(),
secret: z.string().optional()
trustKey: z.string().optional()
})
.strict()
.transform((value) => new GatewayConfiguration(value.repository, value.middlewares, value.monitor, value.secret));
.transform((value) => new GatewayConfiguration(value.repository, value.middlewares, value.monitor, value.trustKey));

export default class GatewayConfiguration extends ProcedureRuntimeConfiguration
{
#monitor?: number;
#repository: string;
#secret?: string;
#trustKey?: string;

constructor(repository: string, middlewares?: string[], monitor?: number, secret?: string)
constructor(repository: string, middlewares?: string[], monitor?: number, trustKey?: string)
{
super(middlewares);

this.#monitor = monitor;
this.#repository = repository;
this.#secret = secret;
this.#trustKey = trustKey;
}

get monitor() { return this.#monitor; }

get repository() { return this.#repository; }

get secret() { return this.#secret; }
get trustKey() { return this.#trustKey; }
}
Loading

0 comments on commit 66f8053

Please sign in to comment.