-
Notifications
You must be signed in to change notification settings - Fork 96
feat(auth): add subject to grant #3440
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
base: main
Are you sure you want to change the base?
Conversation
✅ Deploy Preview for brilliant-pasca-3e80ec ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
🚀 Performance Test ResultsTest Configuration:
Test Metrics:
📜 Logs
|
jest.mock('../access/types', () => ({ | ||
isIncomingPaymentAccessRequest: (access: AccessRequest) => | ||
access.type === 'incoming-payment', | ||
isQuoteAccessRequest: (access: AccessRequest) => access.type === 'quote' | ||
})) |
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 we need this mock?
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.
canSkipInteraction function uses them and the purpose of the unit test is to test only the unit and not the deps, but I see there are also benefits from using the unmocked functions. removed the mock
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.
Overall LGTM, just a couple things
}) | ||
|
||
describe('getByGrant', (): void => { | ||
test('gets access', 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.
nit: this is getting subject, not access
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.
yes, fixed
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.
Few comments!
packages/auth/src/grant/routes.ts
Outdated
400, | ||
GNAPErrorCode.InvalidRequest, | ||
err.message || 'invalid request' |
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.
We should match GrantErrors to their respective HTTP codes and GNAPErrorCodes.
This is similar to how @oana-lolea maps AccessErrors to the respective code & error codes in her PR.
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.
done
packages/auth/src/grant/utils.ts
Outdated
const emptyAccess = (body.access_token?.access?.length ?? 0) === 0 | ||
|
||
if (emptySubject && emptyAccess) { | ||
throw new Error('subject or access_token required') |
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.
Should this throw a GrantError instead?
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.
yes, adapted to throw GrantError
interface IntrospectBody { | ||
access_token: string | ||
access?: AccessItem[] | ||
subjects?: SubjectItem[] |
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 don't use subjects
anywhere (given that we don't pass it in during the introspection request), then its OK to leave out
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.
removed
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 one suggestion to be applied accross all the places where trx
was cast in order to silence a Typescript error.
let appContainer: TestContainer | ||
let accessService: AccessService | ||
let trx: Knex.Transaction | ||
const trx: Knex.Transaction = null as unknown as Knex.Transaction |
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.
It might be healthier to actually initialize trx
in the beforeAll
block so that the tables get properly truncated when the test is finished, but the editor also doesn't flag a Typescript error. I assume this type casting was done to address the TS error.
beforeAll(async (): Promise<void> => {
...
trx = await appContainer.knex.transaction()
})
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.
Yes, It was a fix to a TS error
Variable 'trx' is used before being assigned.ts(2454)
Using a transaction is tricky because it can deadlock tests because some queries are done within a transaction and others are not. Managed to fix it, but in another manner.
let trx: KnexOrTransaction
beforeAll(async (): Promise<void> => {
...
trx = await appContainer.knex
})
Is this ok with you?
let deps: IocContract<AppServices> | ||
let appContainer: TestContainer | ||
let trx: Knex.Transaction | ||
const trx: Knex.Transaction = undefined as unknown as Knex.Transaction |
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.
Ditto to prior comment on initializing trx
.
let deps: IocContract<AppServices> | ||
let appContainer: TestContainer | ||
let trx: Knex.Transaction | ||
const trx: Knex.Transaction = undefined as unknown as Knex.Transaction |
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.
Ditto to prior comment on initializing trx
.
let deps: IocContract<AppServices> | ||
let appContainer: TestContainer | ||
let trx: Knex.Transaction | ||
const trx: Knex.Transaction = undefined as unknown as Knex.Transaction |
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.
Ditto to prior comment on initializing trx
.
let appContainer: TestContainer | ||
let grantService: GrantService | ||
let trx: Knex.Transaction | ||
const trx: Knex.Transaction = null as unknown as Knex.Transaction |
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.
Ditto to prior comment on initializing trx
.
@@ -0,0 +1,86 @@ | |||
import { AccessRequest } from '../access/types' |
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 these tests! Especially since they cover logic that existed before this PR
let interaction: Interaction | ||
let token: AccessToken | ||
let trx: Knex.Transaction | ||
const trx: Knex.Transaction = null as unknown as Knex.Transaction |
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.
Ditto to prior comment on initializing trx
.
let client: string | ||
let amtDelivered: bigint | ||
let trx: Knex.Transaction | ||
const trx: Knex.Transaction = null as unknown as Knex.Transaction |
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.
Ditto to prior comment on initializing trx
.
client: grant.client, | ||
access: grant.access?.map((item) => accessToGraphql(item)), | ||
access: grant.access?.map((item) => accessToGraphql(item)) || [], | ||
subject: grant.subjects?.map((item) => subjectToGraphql(item)) || [], |
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.
grant.subjects
only exists if we add it to the withGraphFetched
request in the service methods OR we have subjects
as a separate "child" resolver under grants, and call the subjectService.getByGrantId
directly.
An example of this is the Asset > sendingFee
resolver in backend
.
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.
fixed with withGraphFetched('subjects')
in the service methods
"Wallet address of the grantee's account." | ||
client: String! | ||
"Details of the access provided by the grant." | ||
access: [Access!]! |
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 think we can keep it as [Access!]!
, if there is no access it can simply be an empty array.
packages/auth/src/grant/routes.ts
Outdated
400, | ||
GNAPErrorCode.InvalidRequest, | ||
'access identifier required' | ||
err instanceof Error ? err.message : '' |
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.
to avoid empty string, let's provide some generic message instead at least. It's less important for the client, its more so for us to find where to debug it.
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.
changed to 'unknown error while checking interaction requirement'
packages/auth/src/subject/service.ts
Outdated
'subject id must be a valid https url' | ||
) | ||
} | ||
if (subject.format != 'uri') { |
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 (subject.format != 'uri') { | |
if (subject.format !== 'uri') { |
packages/auth/src/subject/service.ts
Outdated
|
||
function validateSubjectRequest(subject: SubjectRequest): void { | ||
try { | ||
if (!subject.id.startsWith('https://')) throw 1 |
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.
not sure if @njlie agrees, but I think just checking that it is a valid URL
is sufficient, without checking the https://
. IMO it can be up to the ASE to determine how strict they want to be with the specifics, like protocol.
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.
For the record, I also agree.
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.
fixed
packages/auth/src/grant/model.ts
Outdated
subjectItems: Subject[] | ||
): OpenPaymentsGrant { | ||
return { | ||
access_token: toOpenPaymentsAccessToken(accessToken, accessItems, { |
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.
access_token
could be undefined now, just like subject
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.
fixed
let appContainer: TestContainer | ||
let accessService: AccessService | ||
let trx: Knex.Transaction | ||
let trx: TransactionOrKnex |
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.
We should remove the import { Knex } from 'knex'
import
ctx.body = { | ||
grantId: interaction.grant.id, | ||
access: access.map(toOpenPaymentsAccess), | ||
subjects: subjects.map(toOpenPaymentsSubject), |
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 can only be one subject for a grant, but the sub_ids
can be many. So this can either be a subject
object with sub_ids
array, (like the Open Payments spec), or we can directly return sub_ids
here.
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 same applies for the subject updates in the GraphQL schema
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.
changed to subject object with sub_ids array.
I'm not aware of a subject updates.
const fetchedGrant = await grantService.getByIdWithAccess(grant.id) | ||
const grantRequest: GrantRequest = { | ||
...BASE_GRANT_REQUEST, | ||
subject: { |
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.
access
should still be included here so that this service call can properly test that it also retrieves an access
field along with the grant
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.
fixed
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.
Thinking through this a bit, I think we need to update the response types.
Since subject
should only be returned after we have gone through a successful interaction, we should:
- Make sure we do not return
subject
in the initial grant request - Return
subject
in the grant continue request only
Section 3.4 of GNAP:
The grant request MUST be in the approved state to return this field in the response.
This will require open-payments-specification changes, but we can work together @cozminu and I can take care of those changes
CC @njlie for a double check :)
packages/auth/src/grant/routes.ts
Outdated
err instanceof Error | ||
? err.message | ||
: 'unknown error while checking interaction requirement' |
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.
Looking at what canSkipInteraction
throws, we should instead use err.description
, since that will always be set.
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.
casted e
as GrantError to fix this.
...
} catch (e) {
const err = e as GrantError
throw new GNAPServerRouteError(
400,
GNAPErrorCode.InvalidRequest,
err.message
)
...
Other option would be to throw 500 if e
is not GrantError
packages/auth/src/grant/errors.ts
Outdated
|
||
export class GrantError extends Error { | ||
code: GrantErrorCode | ||
constructor(code: GrantErrorCode, description?: string) { |
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.
constructor(code: GrantErrorCode, description?: string) { | |
constructor(code: GrantErrorCode, description: string) { |
Looks like description
is set everywhere already
packages/auth/src/grant/routes.ts
Outdated
err | ||
errorToHTTPCode[err.code], | ||
errorToGNAPCode[err.code], | ||
err.message || 'invalid request' |
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.
err.message || 'invalid request' | |
err.description || 'invalid request' |
same as the other comment, we can use err.description
here, since it will always be set and is most useful. Or rename GrantError.description
to GrantError.message
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.
renamed description
to message
to fit Error class
packages/auth/src/grant/routes.ts
Outdated
) | ||
} | ||
const access = await deps.accessService.getByGrant(grant.id) | ||
const subjects = await deps.subjectService.getByGrant(grant.id) |
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.
Since subjects
always requires interaction, I think we don't need to fetch subjects
for the non-interactive grant creation.
Based on the quoted excerpt, I thought that the AS could deem subject information something noninteractive - the grant just needed to be approved. But I think ultimately the interpretation is correct, based on the following:
|
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.
Looks good, just a few minor comments
if (!trx) { | ||
await grantTrx.rollback() | ||
} |
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 (!trx) { | |
await grantTrx.rollback() | |
} | |
await grantTrx.rollback() |
as grantTrx
is always defined. Same for L281-283
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.
Tests will fail if I do this. Reason being that the transaction starts on a higher level if trx
is provided and rollback
or commit
will be called twice: once here and once on the higher level.
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.
Looks good! Will just wait for RAF-1147 / #3608
Co-authored-by: Max Kurapov <[email protected]>
@cozminu if you merge in latest changes from |
properties: | ||
access: | ||
$ref: ./auth-server.yaml#/components/schemas/access | ||
subject: |
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.
We should make this spec standalone, and move over the correct components (access/subject) into this file, similar to how it was done for token-introspection
Changes proposed in this pull request
Context
fixes #3343
Checklist
fixes #number
user-docs
label (if necessary)