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 6 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
57 changes: 57 additions & 0 deletions labs/playground1/examples/use-canner-pat-authenticator/vulcan.yaml
Copy link
Contributor

@kokokuo kokokuo Jun 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we create a folder and only provide the vulcan.yaml sample and not contain other SQL, profiles, or API schema files, then your README of the extension-authenticator-canner has done it, so maybe you don't need to add it again ?

Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
###################################################################################################################
# This is a vulcan.yaml example of using extension extension-authenticator-canner to authenticate your API request with Canner PAT
###################################################################################################################
name: playground1
description: A starter Vulcan project
version: 0.1.0-alpha.1
template:
provider: LocalFile
# Path to .sql files
folderPath: sqls
codeLoader: InMemory
artifact:
provider: LocalFile
serializer: JSON
# Path to build result
filePath: result.json
schema-parser:
reader: LocalFile
# Path to .yaml files
folderPath: sqls
document-generator:
specs:
- oas3
types:
- RESTFUL
extensions:
duckdb: '@vulcan-sql/extension-driver-duckdb'
# name my extension as canner-pat
canner-pat: '@vulcan-sql/extension-authenticator-canner'

profiles:
- profile.yaml
rate-limit:
options:
interval:
min: 1
max: 10000
enforce-https:
enabled: false
auth:
enabled: true

response-format:
enabled: true
options:
default: json
formats:
- json
- csv
# here is my definition of canner-pat
canner-pat:
options:
canner-pat:
host: your-canner-host
port: 443
# use https protocol to connect to canner server, change it to false if you are using http
ssl: true
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": {}
}
]
}
46 changes: 46 additions & 0 deletions packages/extension-authenticator-canner/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# 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
```

## Testing

```bash
nx test extension-authenticator-canner
```

This library was generated with [Nx](https://nx.dev).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, you don't need to add the testing section, because the README will wrap to NPM and publish for user reading, so user will install the NPM and won't need the information like other extension packages.

For the testing information, we will provide it in the development documentation in the future.

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