-
Notifications
You must be signed in to change notification settings - Fork 32
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
Conversation
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
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.
Beside some suggestions, others LGTM 👍
const stub = sinon.default.stub( | ||
CannerPATAuthenticator.prototype, | ||
<any>'fetchCannerUser' |
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.
Suggest adding the comment to describe what reason make here should use the prototype to stub the method because it's an unregular way to stub the method of a class.
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.
Btw, here you could also have another way to prevent using the prototype
to stub private method, the solution ( Depends on you, two way both work, just need to add the comment if you use the unregular solution):
- Could create a folder e.g: canner/
- Move the
CannerPATAuthenticator
class to the foldercanner/
- Make
fetchCannerUser
private method to a function expression object, and put it in ahelpers.ts
file, move thehelpers.ts
under thecanner/
, it means thehelpers.ts
only used in the related file under thecanner/
. - import the
fetchCannerUser
function fromhelpers
and call it in theCannerPATAuthenticator
, then you could stub the function in the test cases, and prevent stubbing the private method.
PS: if your private method could be a common method and make multiple places to use, then you could create a folder utils/
under the libs/
and make the private common method a function and put it in a file under the utils/
, and write the test case for the function under the test
You could refer to the flattenElements.ts and https://github.com/Canner/vulcan-sql/blob/2b5e282aa047cfaff1c80471e2e4bca119bde769/packages/core/src/lib/utils/normalizedStringValue.ts under the core
package, we also write the tests case for them under the test/utils
:)
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.
Thanks for the solution!
I'll add some comments for now, and I think we can encapsulate how to make requests to Canner and move it to utils
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.
Do you forget to add the // TODO: refactor to avoid stubbing private method
?
// const { host, port } = this.options; | ||
// if (this.options && (!host || !port)) | ||
// throw new ConfigurationError('please provide canner "host" and "port".'); |
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.
Do you forget to remove the code ?
const res = await this.fetchCannerUser(token); | ||
if (res.status == 401) throw new UserError('invalid token'); | ||
if (!res.data.data?.userMe) { | ||
throw new InternalError('Can not retrieve user info from canner server'); |
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.
The Axios will throw the error if the HTTP code is not 200, so if you could like to check the 401
.
You may need to handle the code in the fetchCannerUser
method through try-catch and check the status
to throw UserError
and check the userMe
together, of couse, you could catch the error in fetchCannerUser
and transform the response and return data to make your validate
to check 40
and userMe
.
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.
Do you miss the part ? Or did I miss some situation from canner enterprise?
I saw you update the fetchCannerUser
method, but since you throw the error from my suggestion, so you wouldn't trigger the conditions if(res.status == 401) throw new UserError('invalid token');
Btw: In JS or TS, the equal should be use ===
, not ==
, it will have different result.
Otherwise, just curious what situation do you call the userMe
and got the status = 200, but userMe
is undefined or {}
?
In graphQL, when you got 200
, because you have typed the GraphQL response named userMe
and it's a field, it must get the result in general.
If you do not type the field, the graphQL syntax error will throw an error like the below:
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.
I'll remove this part case I've handled the error axios throw that the response status is not 2xx in fetchCannerUser.
private async fetchCannerUser(token: string) { | ||
const graphqlUrl = this.getCannerUrl('/web/graphql'); | ||
try { | ||
return await axios.default.post( |
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.
Why do you need to call post by axios.default.post
? because calling axios.post
directly is the more common way.
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.
I use import * as axios from 'axios'
, I'll change the import and use axios.post directly
throw new InternalError( | ||
`Failed to fetch user info from canner server: ${error}` | ||
); |
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.
Just curious, would you like to print the error's message or the whole content of the error object?
You use the template string way to print the message with error
object, and Axios will throw the AxiosError
object, so the template string way have printed the [object Object]
possibly, not sure if have you tested the error throw result ?
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.
I'll modify this part, and handle the error the same as #172 does
// clear stub after each test | ||
afterEach(() => { | ||
sinon.default.restore(); | ||
}); | ||
|
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.
Seems in the file, you didn't use the sinon
to stub
? so actually you don't add the restore
?
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.
I'll remove it from the test file.
import { CannerPATAuthenticator } from '@vulcan-sql/serve/auth'; | ||
import { AuthResult, AuthStatus, KoaContext } from '@vulcan-sql/serve/models'; | ||
|
||
const wrappedAuthCredential = async ( |
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.
Wrap the function to reuse it great!
However, according to your test cases for asserting the error, you should prevent including other non-target test logistics, like here you create the authenticator and called the activate method, it is not the target act code section even though these create heavier and activate behavior seems not possibly cause others errors.
sinon.default | ||
.stub(authenticator, <any>'fetchCannerUser') | ||
.resolves(resolveValue); |
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.
Same suggestion as above using prototype
way.
const authenticator = new CannerPATAuthenticator({ options }, ''); | ||
await authenticator.activate(); | ||
return await authenticator.getTokenInfo(ctx); |
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.
Same suggestion as above creating the authenticator and calling activate method with the target method at the same place.
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.
I've removed this function because it is not used after refactoring.
ssl: false, | ||
}, | ||
}; | ||
const invalidToken = Buffer.from('clientId:clientSecret').toString('base64'); |
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.
great way to create the invalid token and also make members know the token content pattern.
- add Canner PAT authenticator - add unit tests - add integration tests
7420552
to
73cee37
Compare
…axios error handling
73cee37
to
824d133
Compare
Codecov ReportPatch coverage:
❗ Your organization is not using the GitHub App Integration. As a result you may experience degraded service beginning May 15th. Please install the Github App Integration for your organization. Read more. Additional details and impacted files@@ Coverage Diff @@
## develop #181 +/- ##
===========================================
- Coverage 91.06% 90.99% -0.07%
===========================================
Files 328 331 +3
Lines 5393 5430 +37
Branches 716 725 +9
===========================================
+ Hits 4911 4941 +30
- Misses 343 345 +2
- Partials 139 144 +5
Flags with carried forward coverage won't be shown. Click here to find out more.
☔ View full report in Codecov by Sentry. |
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.
Besides some questions and suggestions, others LGTM 👍
Btw, we should move our canner PAT authenticator to a new extension package like canner driver, because canner integration is an optional (enhancement) feature for VulcanSQL.
Keep going 👍
const stub = sinon.default.stub( | ||
CannerPATAuthenticator.prototype, | ||
<any>'fetchCannerUser' |
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.
Do you forget to add the // TODO: refactor to avoid stubbing private method
?
export interface CannerPATOptions { | ||
host: string; | ||
port: number; | ||
// default is false |
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.
Great comment to make other members know the default value 👍
} | ||
const cannerUser = res.data.data?.userMe; | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-unused-vars |
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.
Why add the ESlint comment? Because seems you could use the username
directly, see below:
const { username, ...restAttrs } = cannerUser;
return {
status: AuthStatus.SUCCESS,
type: this.getExtensionId()!, // method name
user: {
name: username,
attr: restAttrs,
},
} as AuthResult;
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.
I'll remove the Eslint comment here
private getCannerUrl = (path = '/') => { | ||
const { host, port, ssl = false } = this.options; | ||
if (process.env['IS_ON_KUBERNETES']) | ||
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}${path}`; | ||
} |
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.
Why use the function expression in the getCannerUrl
?
If we use the class, we should follow the regular format to write the private method:
private getCannerUrl(path: string = '/') {
....
}
Otherwise, we should prevent to use of the function expression for the public method or private method for good habit, or you may face the issues possibly in your career:
- Inheritance issue
- Multiple decorators issue
- Performance issue
Please read our 7. Not use function expression to define methods in a class at Backend Convention document, we also provide the reference link it describe more clearly.
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.
Thanks for the advice! I'll take a look at the document.
const res = await this.fetchCannerUser(token); | ||
if (res.status == 401) throw new UserError('invalid token'); | ||
if (!res.data.data?.userMe) { | ||
throw new InternalError('Can not retrieve user info from canner server'); |
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.
Do you miss the part ? Or did I miss some situation from canner enterprise?
I saw you update the fetchCannerUser
method, but since you throw the error from my suggestion, so you wouldn't trigger the conditions if(res.status == 401) throw new UserError('invalid token');
Btw: In JS or TS, the equal should be use ===
, not ==
, it will have different result.
Otherwise, just curious what situation do you call the userMe
and got the status = 200, but userMe
is undefined or {}
?
In graphQL, when you got 200
, because you have typed the GraphQL response named userMe
and it's a field, it must get the result in general.
If you do not type the field, the graphQL syntax error will throw an error like the below:
): Promise<CannerPATAuthenticator> => { | ||
const authenticator = new CannerPATAuthenticator({ options }, ''); | ||
if (stubValue) { | ||
// TODO: refactor to avoid stubbing private method |
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.
Thanks for adding the TODO
for refactoring in the future and the comment 👍
mockedAxios.post.mockRejectedValue({ | ||
request: 'mock request', | ||
response: { | ||
data: { error: 'mock response error' }, | ||
status: 500, | ||
statusText: 'string', | ||
headers: {}, | ||
config: {}, | ||
} as any, | ||
isAxiosError: true, | ||
toJSON: () => ({}), | ||
message: 'An error occurred!', | ||
name: 'Error', | ||
}); |
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.
Why do you need to mock the axios.post
reject value ? You could mock the rejected value for fetchCannerUser
directly ?
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.
After discussion, in actually, @onlyjackfrost would like to test the fetchCannerUser
's catch and transform part, but currently fetchCannerUser
is a private method that is called by the private validate
method, so that why he needs to mock the axio.post
, so our conclusion is to update the description fo the test case to make others know the intention.
const mockResolveValue = { | ||
status: 401, | ||
data: { error: 'invalid token from canner' }, | ||
}; | ||
const authenticator = await getStubAuthenticator( | ||
mockOptions, | ||
mockResolveValue | ||
); |
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.
Just curious why do you pass the mockResolveValue
be a resolved value for stub fetchCannerUser
?
Seems the fetchCannerUser
won't return the data whose status is 401
, because when Axios get the 401,
like the above question in the validate
I asked. Or did I miss some situations from Canner Enterprise?
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.
I'll remove the test cases that are testing the remote response whose status is not 200, and keep the test case that is testing the error handling of axios.
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.
I missed some questions, so I append two in here
operationName: 'UserMe', | ||
variables: {}, | ||
query: | ||
'query UserMe{userMe {accountRole attributes createdAt email groups {id name} lastName firstName username', |
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.
operationName: 'UserMe', | ||
variables: {}, | ||
query: | ||
'query UserMe{userMe {accountRole attributes createdAt email groups {id name} lastName firstName username', |
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.
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.
These attributes were picked after discussing with William.
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.
Thanks for checking the attribute
field and that us know it !👍
…sed code and refine test cases
… module to extension
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.
Besides 3 suggestions, others LGTM 👍
Btw, here are 3 PR descriptions for you :)
-
Suggest to add the introduction of how to use Canner PAT extension and its configration in PR descriotion, because it is a new feature, like other PR: Feature: Support Catalog User Interface #160, Feature: Dynamic data masking - custom string #140, Feature: Passing route information to template engine #142, Feature: Syntax sugars for parameter validators #108
-
Your current NOTE seems to explain the reason we have the
options
issue but does not mention you solve it, so according to your PR description and title, not know you solve the issue. -
Suggest you could add another screenshot is that not type
options
, or other users look your PR may consider we must addoptions
auth:
enabled: true
# looks like the options also could not add it, right ?
options:
``
## 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 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.
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 the extension-authenticator-canner
has done it, so maybe you don't need to add it again ?
}); | ||
|
||
// Token info | ||
it('Should throw when use getTokenInfo with cannerPATAuthenticator', async () => { |
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.
Do you miss the error
word => Should throw error...
?
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.
Thanks for fixing it, Well done, LGTM 👍
Description
In this pull request, I implemented an extension "extension-authenticator-canner" that enabled Vulcan API to authenticate users with Canner Enterprise and retrieve the user information from Canner Enterprise. We can access these user attributes after authentication.
Please see the README to know how to setup the extension.
Issue ticket number
#177
Note
We changed the auth configure checking logic to if "auth.enabled" is set to true but didn't configure any authenticator, we will not throw a configured error when serving but throw "all types of authenticator failed" when sending Data API(with or without "Authorization" header or any replacement field/header).
Test result:
labs/playground

only pat
only basic auth

auth is enabled but not configured
