-
Notifications
You must be signed in to change notification settings - Fork 27
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
Changes from 6 commits
4bd1b29
824d133
08aa72d
efdacb0
94a636e
3d3ab85
646b3d8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 |
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": {} | ||
} | ||
] | ||
} |
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). | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
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', | ||
}; |
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" | ||
} | ||
} |
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": [] | ||
} |
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'; |
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}`; | ||
} | ||
} | ||
} |
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; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './authenticator/pat'; |
There was a problem hiding this comment.
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 theextension-authenticator-canner
has done it, so maybe you don't need to add it again ?