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

Feature: Canner PAT authenticator #181

Merged
merged 7 commits into from
Jun 13, 2023
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
9 changes: 8 additions & 1 deletion labs/playground1/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ start: build test-data/moma.db ../../node_modules
@vulcan start

# build the required packages
build: pkg-core pkg-build pkg-serve pkg-catalog-server pkg-cli pkg-extension-driver-duckdb
build: pkg-core pkg-build pkg-serve pkg-catalog-server pkg-cli pkg-extension-driver-duckdb pkg-extension-authenticator-canner


# build for core pakge
Expand Down Expand Up @@ -49,6 +49,13 @@ pkg-extension-driver-duckdb: ../../node_modules
rm -rf ./labs/playground1/node_modules/@vulcan-sql/extension-driver-duckdb; \
cp -R ./dist/packages/extension-driver-duckdb ./labs/playground1/node_modules/@vulcan-sql

pkg-extension-authenticator-canner: ../../node_modules
@cd ../..; \
yarn nx build extension-authenticator-canner; \
mkdir -p ./labs/playground1/node_modules/@vulcan-sql; \
rm -rf ./labs/playground1/node_modules/@vulcan-sql/extension-authenticator-canner; \
cp -R ./dist/packages/extension-authenticator-canner ./labs/playground1/node_modules/@vulcan-sql

# build and install for cli pakge
pkg-cli: ../../node_modules
@cd ../..; \
Expand Down
4 changes: 4 additions & 0 deletions labs/playground1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,7 @@ make
## Testing Data

After installation, you can find `artists.csv` and `artworks.csv` under folder `test-data`. They are the data we used for this playground. You can also access the data base via [DuckDB CLI](https://duckdb.org/docs/api/cli): `duckdb ./test-data/moma.db`

## Examples

We provide some examples in the `examples` folder to show how to configured your Vulcan API
18 changes: 18 additions & 0 deletions packages/extension-authenticator-canner/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"extends": ["../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}
38 changes: 38 additions & 0 deletions packages/extension-authenticator-canner/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# extension-authenticator-canner

This extension make Data API(VulcanSQL API) can integrate with [Canner Enterprise](https://cannerdata.com/product) and use Canner as a authenticate server

This extension let Data API request can be authenticated with [Canner PAT](https://docs.cannerdata.com/product/api_sdk/api_personal_access_token)

## Install

1. Install package

```sql
npm i @vulcan-sql/extension-authenticator-canner
```

2. Update `vulcan.yaml`, enable the extension and enable the `auth` configuration.

```yaml
auth:
enabled: true
# The extension-authenticator-canner and [build-in authenticator](https://vulcansql.com/docs/data-privacy/authn) can work at the same time

extensions:
canner-authenticator: '@vulcan-sql/extension-authenticator-canner'
```

3. Update `vulcan.yaml`, define your `canner-authenticator`
```yaml
canner-authenticator:
# To having the same config structure to the authenticator middleware, we
options:
canner-pat:
# your canner enterprise host
host: 'my-canner-host-dns'
# your canner enterprise post
post: 443
# indicate using http or https default is false
ssl: true
```
14 changes: 14 additions & 0 deletions packages/extension-authenticator-canner/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module.exports = {
displayName: 'extension-authenticator-canner',
preset: '../../jest.preset.ts',
globals: {
'ts-jest': {
tsconfig: '<rootDir>/tsconfig.spec.json',
},
},
transform: {
'^.+\\.[tj]s$': 'ts-jest',
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../coverage/packages/extension-authenticator-canner',
};
30 changes: 30 additions & 0 deletions packages/extension-authenticator-canner/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "@vulcan-sql/extension-authenticator-canner",
"description": "Canner Enterprise authenticator for Vulcan SQL",
"version": "0.4.0",
"type": "commonjs",
"publishConfig": {
"access": "public"
},
"keywords": [
"vulcan",
"vulcan-sql",
"data",
"sql",
"database",
"data-warehouse",
"data-lake",
"api-builder",
"postgres",
"pg"
],
"repository": {
"type": "git",
"url": "https://github.com/Canner/vulcan.git"
},
"license": "MIT",
"peerDependencies": {
"@vulcan-sql/core": "~0.4.0-0",
"@vulcan-sql/serve": "~0.4.0-0"
}
}
32 changes: 32 additions & 0 deletions packages/extension-authenticator-canner/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"root": "packages/extension-authenticator-canner",
"sourceRoot": "packages/extension-authenticator-canner/src",
"targets": {
"build": {
"executor": "@nrwl/js:tsc",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/packages/extension-authenticator-canner",
"main": "packages/extension-authenticator-canner/src/index.ts",
"tsConfig": "packages/extension-authenticator-canner/tsconfig.lib.json",
"assets": ["packages/extension-authenticator-canner/*.md"]
}
},
"lint": {
"executor": "@nrwl/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["packages/extension-authenticator-canner/**/*.ts"]
}
},
"test": {
"executor": "@nrwl/jest:jest",
"outputs": ["coverage/packages/extension-authenticator-canner"],
"options": {
"jestConfig": "packages/extension-authenticator-canner/jest.config.ts",
"passWithNoTests": true
}
}
},
"tags": []
}
3 changes: 3 additions & 0 deletions packages/extension-authenticator-canner/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { CannerPATAuthenticator } from './lib';

export default [CannerPATAuthenticator];
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './pat';
120 changes: 120 additions & 0 deletions packages/extension-authenticator-canner/src/lib/authenticator/pat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import {
ConfigurationError,
VulcanExtensionId,
InternalError,
} from '@vulcan-sql/core';
import {
BaseAuthenticator,
KoaContext,
AuthStatus,
AuthResult,
} from '@vulcan-sql/serve';
import { isEmpty } from 'lodash';
import axios from 'axios';
import config from '../config';

export interface CannerPATOptions {
host: string;
port: number;
// default is false
ssl: boolean;
}

@VulcanExtensionId('canner-pat')
export class CannerPATAuthenticator extends BaseAuthenticator<CannerPATOptions> {
private options: CannerPATOptions = {} as CannerPATOptions;

public override async onActivate() {
this.options = this.getOptions() as CannerPATOptions;
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
public async getTokenInfo(ctx: KoaContext): Promise<any> {
throw new InternalError(`canner-pat does not support token generate.`);
}

public async authCredential(context: KoaContext) {
const incorrect = {
status: AuthStatus.INDETERMINATE,
type: this.getExtensionId()!,
};
const authorize = context.request.headers['authorization'];
if (
// no need to check this.options because it a external extension,
// it must be configured correctly to be load into container and can be used in authenticate middleware
!authorize ||
!authorize.toLowerCase().startsWith(this.getExtensionId()!)
)
return incorrect;

if (isEmpty(this.options) || !this.options.host)
throw new ConfigurationError(
'please provide correct connection information to Canner Enterprise, including "host".'
);

// validate request auth token
const token = authorize.trim().split(' ')[1];

try {
return await this.validate(token);
} catch (err) {
return {
status: AuthStatus.FAIL,
type: this.getExtensionId()!,
message: (err as Error).message,
};
}
}

private async validate(token: string) {
const res = await this.fetchCannerUser(token);
const cannerUser = res.data.data?.userMe;
const { username, ...restAttrs } = cannerUser;
return {
status: AuthStatus.SUCCESS,
type: this.getExtensionId()!, // method name
user: {
name: username,
attr: restAttrs,
},
} as AuthResult;
}

private async fetchCannerUser(token: string) {
const graphqlUrl = this.getCannerUrl('/web/graphql');
try {
return await axios.post(
graphqlUrl,
{
operationName: 'UserMe',
variables: {},
query:
'query UserMe{userMe {accountRole attributes createdAt email groups {id name} lastName firstName username}}',
},
{
headers: {
Authorization: `Token ${token}`,
},
}
);
} catch (error: any) {
const message = error.response
? `response status: ${
error.response.status
}, response data: ${JSON.stringify(error.response.data)}`
: `remote server does not response. request ${error.toJSON()}}`;
throw new InternalError(
`Failed to fetch user info from canner server: ${message}`
);
}
}
private getCannerUrl(path = '/') {
const { host, port, ssl = false } = this.options;
if (config.isOnKubernetes)
return `http://${process.env['WEB_SERVICE_HOST']}${path}`; // for internal usage, we don't need to specify port
else {
const protocol = ssl ? 'https' : 'http';
return `${protocol}://${host}${port ? `:${port}` : ''}${path}`;
}
}
}
10 changes: 10 additions & 0 deletions packages/extension-authenticator-canner/src/lib/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export interface IEnvConfig {
// indicates whether the extension is running in k8s
isOnKubernetes?: boolean;
}

const config: IEnvConfig = {
isOnKubernetes: Boolean(process.env['IS_ON_KUBERNETES']) || false,
};

export default config;
1 change: 1 addition & 0 deletions packages/extension-authenticator-canner/src/lib/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './authenticator/pat';
Loading