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

feat: include new validation on validateAdminUserAccess #181

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## [Unreleased]

### Added

- Added optional argument to validateAdminUserAccess and validateStoreUserAccess directives to check specific user permissions via LM API.

## [0.61.0] - 2024-10-16

### Added
Expand Down
8 changes: 6 additions & 2 deletions graphql/directives.graphql
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
directive @withSession on FIELD | FIELD_DEFINITION
directive @withPermissions on FIELD | FIELD_DEFINITION
directive @validateAdminUserAccess on FIELD | FIELD_DEFINITION
directive @validateStoreUserAccess on FIELD | FIELD_DEFINITION
directive @validateAdminUserAccess(
requiredPermission: String
) on FIELD | FIELD_DEFINITION
directive @validateStoreUserAccess(
requiredPermission: String
) on FIELD | FIELD_DEFINITION
directive @checkUserAccess on FIELD | FIELD_DEFINITION
directive @checkAdminAccess on FIELD | FIELD_DEFINITION
directive @auditAccess on FIELD | FIELD_DEFINITION
29 changes: 19 additions & 10 deletions graphql/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ type Query {
sortedBy: String = "name"
): OrganizationResult
@cacheControl(scope: PRIVATE, maxAge: SHORT)
@validateStoreUserAccess
@validateStoreUserAccess(requiredPermission: "buyer_organization_view")

getOrganizationsWithoutSalesManager: [Organization]
@cacheControl(scope: PRIVATE)
@validateAdminUserAccess
@validateAdminUserAccess(requiredPermission: "buyer_organization_view")

getOrganizationById(id: ID): Organization
@cacheControl(scope: PRIVATE)
Expand Down Expand Up @@ -109,11 +109,15 @@ type Query {
getB2BSettings: B2BSettings
@cacheControl(scope: PUBLIC, maxAge: SHORT)
@auditAccess
getSellers: [Seller] @validateAdminUserAccess @cacheControl(scope: PRIVATE)
getSellers: [Seller]
@validateAdminUserAccess(requiredPermission: "buyer_organization_view")
@cacheControl(scope: PRIVATE)
getSellersPaginated(page: Int, pageSize: Int): GetSellersPaginatedResponse
@validateAdminUserAccess
@validateAdminUserAccess(requiredPermission: "buyer_organization_view")
@cacheControl(scope: PRIVATE)
getAccount: Account
@validateAdminUserAccess(requiredPermission: "buyer_organization_view")
@cacheControl(scope: PRIVATE)
getAccount: Account @validateAdminUserAccess @cacheControl(scope: PRIVATE)
}

type Account {
Expand Down Expand Up @@ -153,7 +157,7 @@ type Mutation {
notifyUsers: Boolean
): OrganizationCostCenterResponse
@checkAdminAccess
@cacheControl(scope: PRIVATE)
@cacheControl(scope: PRIVATE, maxAge: 300)
createOrganizationAndCostCentersWithId(
input: NormalizedOrganizationInput!
): MasterDataResponse @checkAdminAccess @cacheControl(scope: PRIVATE)
Expand All @@ -164,6 +168,7 @@ type Mutation {
@withSession
@withPermissions
@checkUserAccess
@validateAdminUserAccess(requiredPermission: "buyer_organization_edit")
@cacheControl(scope: PRIVATE)
createCostCenterWithId(
organizationId: ID
Expand Down Expand Up @@ -225,7 +230,7 @@ type Mutation {
): MutationResponse
@withSession
@withPermissions
@validateStoreUserAccess
@validateStoreUserAccess(requiredPermission: "buyer_organization_edit")
@cacheControl(scope: PRIVATE)
"""
addUser will create an user with the provided parameters.
Expand Down Expand Up @@ -267,12 +272,16 @@ type Mutation {
name: String!
email: String!
canImpersonate: Boolean = false
): MutationResponse @validateAdminUserAccess @cacheControl(scope: PRIVATE)
): MutationResponse
@validateAdminUserAccess(requiredPermission: "buyer_organization_edit")
@cacheControl(scope: PRIVATE)
removeUserWithEmail(
orgId: ID!
costId: ID!
email: String!
): MutationResponse @validateAdminUserAccess @cacheControl(scope: PRIVATE)
): MutationResponse
@validateAdminUserAccess(requiredPermission: "buyer_organization_edit")
@cacheControl(scope: PRIVATE)
updateUser(
id: ID
roleId: ID!
Expand Down Expand Up @@ -317,7 +326,7 @@ type Mutation {
saveB2BSettings(input: B2BSettingsInput): MutationResponse
@withSession
@withPermissions
@validateAdminUserAccess
@validateAdminUserAccess(requiredPermission: "buyer_organization_edit")
}

type SettingsResponse {
Expand Down
10 changes: 9 additions & 1 deletion node/clients/IdentityClient.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import type { InstanceOptions, IOContext } from '@vtex/api'
import { JanusClient } from '@vtex/api'

type AuthUser = {
id: string
authStatus: string
tokenType: string
account: string
audience: string
user: string
}
export default class IdentityClient extends JanusClient {
constructor(ctx: IOContext, options?: InstanceOptions) {
super(ctx, {
Expand All @@ -11,7 +19,7 @@ export default class IdentityClient extends JanusClient {
})
}

public async validateToken({ token }: { token: string }): Promise<any> {
public async validateToken({ token }: { token: string }): Promise<AuthUser> {
return this.http.post('/api/vtexid/credential/validate', { token })
}

Expand Down
22 changes: 21 additions & 1 deletion node/clients/LMClient.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { InstanceOptions, IOContext } from '@vtex/api'
import { ExternalClient } from '@vtex/api'
import { ExternalClient, ForbiddenError } from '@vtex/api'

import { B2B_LM_PRODUCT_CODE } from '../utils/constants'

export default class LMClient extends ExternalClient {
constructor(ctx: IOContext, options?: InstanceOptions) {
Expand All @@ -24,6 +26,24 @@ export default class LMClient extends ExternalClient {
})
}

public checkAdminUserRequiredPermission = async (
account: string,
userEmail: string,
resourceCode: string
) => {
const productCode = B2B_LM_PRODUCT_CODE // resource name on lincense manager = Buyer Organization

const checkrequiredPermission = await this.get<boolean>(
`/api/license-manager/pvt/accounts/${account}/products/${productCode}/logins/${userEmail}/resources/${resourceCode}/granted`
)

if (!checkrequiredPermission) {
throw new ForbiddenError('Unauthorized Access')
}

return checkrequiredPermission
}

public getAccount = async () => {
return this.get<GetAccountResponse>(`/api/license-manager/account`).then(
(res) => {
Expand Down
19 changes: 16 additions & 3 deletions node/resolvers/directives/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { isUserPartOfBuyerOrg } from '../Queries/Users'

export const validateAdminToken = async (
context: Context,
adminUserAuthToken: string
adminUserAuthToken: string,
requiredPermission?: 'buyer_organization_edit' | 'buyer_organization_view'
): Promise<{
hasAdminToken: boolean
hasValidAdminToken: boolean
Expand All @@ -16,12 +17,12 @@ export const validateAdminToken = async (
// check if has admin token and if it is valid
const hasAdminToken = !!adminUserAuthToken
let hasValidAdminToken = false
// this is used to check if the token is valid by current standards
let hasCurrentValidAdminToken = false
let authUser: any

if (hasAdminToken) {
try {
const authUser = await identity.validateToken({
authUser = await identity.validateToken({
token: adminUserAuthToken,
})

Expand All @@ -44,6 +45,18 @@ export const validateAdminToken = async (
}
}

if (
hasValidAdminToken &&
requiredPermission &&
authUser.tokenType === 'user'
) {
await lm.checkAdminUserRequiredPermission(
account,
authUser.user,
requiredPermission
)
}

return { hasAdminToken, hasValidAdminToken, hasCurrentValidAdminToken }
}

Expand Down
5 changes: 4 additions & 1 deletion node/resolvers/directives/validateAdminUserAccess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export class ValidateAdminUserAccess extends SchemaDirectiveVisitor {
public visitFieldDefinition(field: GraphQLField<any, any>) {
const { resolve = defaultFieldResolver } = field

const { requiredPermission } = this.args

field.resolve = async (
root: any,
args: any,
Expand Down Expand Up @@ -43,7 +45,8 @@ export class ValidateAdminUserAccess extends SchemaDirectiveVisitor {

const { hasAdminToken, hasValidAdminToken } = await validateAdminToken(
context,
adminUserAuthToken as string
adminUserAuthToken as string,
requiredPermission
)

// add admin token metrics
Expand Down
4 changes: 3 additions & 1 deletion node/resolvers/directives/validateStoreUserAccess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
export class ValidateStoreUserAccess extends SchemaDirectiveVisitor {
public visitFieldDefinition(field: GraphQLField<any, any>) {
const { resolve = defaultFieldResolver } = field
const { requiredPermission } = this.args

field.resolve = async (
root: any,
Expand Down Expand Up @@ -44,7 +45,8 @@ export class ValidateStoreUserAccess extends SchemaDirectiveVisitor {

const { hasAdminToken, hasValidAdminToken } = await validateAdminToken(
context,
adminUserAuthToken as string
adminUserAuthToken as string,
requiredPermission
)

// add admin token metrics
Expand Down
2 changes: 2 additions & 0 deletions node/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ export const ORGANIZATION_REQUEST_STATUSES = {
export const MARKETING_TAGS = {
VBASE_BUCKET: 'b2b_marketing_tags',
}

export const B2B_LM_PRODUCT_CODE = '97' // resource name on lincense manager = Buyer Organization