diff --git a/cypress/e2e/specs/spec_renderer.spec.ts b/cypress/e2e/specs/spec_renderer.spec.ts
index 2f410406..2b401fcc 100644
--- a/cypress/e2e/specs/spec_renderer.spec.ts
+++ b/cypress/e2e/specs/spec_renderer.spec.ts
@@ -1,3 +1,4 @@
+import { ProductActionsResponse } from '@kong/sdk-portal-js'
import { product, versions } from '../fixtures/consts'
import petstoreJson from '../fixtures/oas_specs/petstoreJson.json'
import petstoreJson3 from '../fixtures/oas_specs/petstoreJson3.0.json'
@@ -330,18 +331,23 @@ describe('Spec Renderer Page', () => {
rbac_enabled: true
}).as('getPortalContext')
- cy.intercept('GET', 'api/v2/portals/*/developers/me/permissions', {
+ const response: ProductActionsResponse = {
+ actions: {
+ register: false,
+ view: false,
+ view_documentation: false
+ }
+ }
+
+ cy.intercept('GET', '/api/v2/products/*/actions', {
statusCode: 200,
- body: [{
- resource: 'krn:konnect:reg/*:org/*:portals/*/services/*',
- actions: []
- }],
+ body: response,
delay: 300
- }).as('getPermissions')
+ }).as('getProductActions')
cy.visit(`/spec/${product.id}`)
- cy.wait('@getPermissions')
+ cy.wait('@getProductActions')
cy.get('[data-testid="forbidden"]').should('exist')
})
@@ -351,21 +357,23 @@ describe('Spec Renderer Page', () => {
rbac_enabled: true
}).as('getPortalContext')
- cy.intercept('GET', 'api/v2/portals/*/developers/me/permissions', {
+ const response: ProductActionsResponse = {
+ actions: {
+ register: true,
+ view: true,
+ view_documentation: true
+ }
+ }
+
+ cy.intercept('GET', '/api/v2/products/*/actions', {
statusCode: 200,
- body: [{
- resource: 'krn:konnect:reg/*:org/*:portals/*/services/*',
- actions: [
- '#view',
- '#consume'
- ]
- }],
+ body: response,
delay: 300
- }).as('getPermissions')
+ }).as('getProductActions')
cy.visit(`/spec/${product.id}`)
- cy.wait('@getPermissions')
+ cy.wait('@getProductActions')
cy.get('[data-testid="kong-public-ui-spec-details-swagger"]', { timeout: 12000 })
.get('.info h2').should('contain', 'Swagger Petstore')
@@ -373,17 +381,17 @@ describe('Spec Renderer Page', () => {
cy.get('[data-testid="register-button"]').should('exist')
})
- it('does not call developers/me/permissions if rbac not enabled', () => {
+ it('does not retrieve product actions if rbac not enabled', () => {
cy.intercept('GET', '**/api/v2/portal', {
rbac_enabled: false
}).as('getPortalContext')
- cy.intercept('get', 'api/v2/portals/*/developers/me/permissions', cy.spy().as('apiNotCalled'))
+ cy.intercept('get', '/api/v2/products/*/actions', cy.spy().as('apiNotCalled'))
cy.visit(`/spec/${product.id}`)
cy.get('[data-testid="kong-public-ui-spec-details-swagger"]', { timeout: 12000 })
- .get('.info h2').should('contain', 'Swagger Petstore')
+ .get('.info h2').should('contain', 'Swagger Petstore')
cy.get('[data-testid="register-button"]').should('exist')
@@ -405,12 +413,12 @@ describe('Spec Renderer Page', () => {
cy.mockAppearance()
})
- it('allows seeing spec when portal is public and rbac enabled, does not call developers/me/permissions', () => {
+ it('allows seeing spec when portal is public and rbac enabled, does not retrieve product actions', () => {
cy.intercept('GET', '**/portal_api/portal/portal_context', {
rbac_enabled: true
}).as('getPortalContext')
- cy.intercept('get', 'api/v2/portals/*/developers/me/permissions', cy.spy().as('apiNotCalled'))
+ cy.intercept('get', '/api/v2/products/*/actions', cy.spy().as('apiNotCalled'))
cy.visit(`/spec/${product.id}`)
diff --git a/package.json b/package.json
index 65b12c44..8de3a691 100644
--- a/package.json
+++ b/package.json
@@ -32,7 +32,7 @@
"@kong-ui-public/document-viewer": "0.10.5",
"@kong-ui-public/spec-renderer": "0.13.1",
"@kong/kong-auth-elements": "2.8.0",
- "@kong/kongponents": "8.123.3",
+ "@kong/kongponents": "8.126.1",
"@kong/sdk-portal-js": "2.3.6",
"@xstate/vue": "2.0.0",
"axios": "1.6.0",
diff --git a/src/components/AuthValidate.vue b/src/components/AuthValidate.vue
deleted file mode 100644
index 90781396..00000000
--- a/src/components/AuthValidate.vue
+++ /dev/null
@@ -1,91 +0,0 @@
-
diff --git a/src/helpers/permissions.ts b/src/helpers/permissions.ts
deleted file mode 100644
index 23eb0184..00000000
--- a/src/helpers/permissions.ts
+++ /dev/null
@@ -1,80 +0,0 @@
-/**
- * Determine if the resource path from a KRN from the API matches a requested resource path guard. Substitutes UUID for `*` matches.
- * @param {ParsedKrn} parsedKrn A parsed krn (from the API)
- * @param {string} requestedResourcePath The resource path being accessed
- * @returns {boolean}
- */
-export const resourcePathMatches = (parsedKrn, requestedResourcePath: string) => {
- const parsedKrnResourcePathArray = parsedKrn?.resourcePath?.split('/')
- const requestedResourcePathArray = requestedResourcePath?.split('/')
-
- // If the krns do not have the same path, exit early
- if (parsedKrnResourcePathArray?.length !== requestedResourcePathArray?.length) {
- return false
- }
-
- const pathsMatch = []
-
- requestedResourcePathArray?.forEach((requestedPathValue, index) => {
- // Set the pathMatches to false by default
- let pathMatches = false
-
- // If odd index, requestedPathValue is UUID or wildcard '*'
- if (index % 2) {
- pathMatches = requestedPathValue === parsedKrnResourcePathArray[index] || parsedKrnResourcePathArray[index] === '*' && requestedPathValue !== ''
- } else {
- // If index is even number, requestedPathValue is the static entity, e.g. 'services', 'runtimegroups'
- pathMatches = requestedPathValue === parsedKrnResourcePathArray[index]
- }
-
- // Push boolean (true/false) depending on if either globMatches and entityMatches === true
- pathsMatch.push(pathMatches)
- })
-
- return pathsMatch?.every(matchFromPath => matchFromPath === true) || false
-}
-
-/**
- * @description Does the krnArg include the required properties and have a valid resource path
- * @param {(RequestedPermissionKrn|RequestedPermissionDictionary)} krnArg The object to validate
- * @return {*} {boolean}
- */
-
-export const krnArgIsValid = (krnArg) => {
- // If all object properties are valid krn args, and required properties are set
- // and ensure args.resourcePath does not include invalid characters in the path
- return objectIsKrnArg(krnArg) && krnResourcePathIsValid(krnArg.resourcePath)
-}
-
-/**
- * @description Returns true if the potentialKrnArgs object is of type RequestedPermissionKrn and not a dictionary
- * @param {(RequestedPermissionKrn|RequestedPermissionDictionary)} potentialKrnArgs The object to validate
- * @return {*} {boolean} Is the object a single krn arg, rather than a dictionary
- */
-
-export const objectIsKrnArg = (potentialKrnArgs) => {
- const keys = Object.keys(potentialKrnArgs)
- const values = Object.values(potentialKrnArgs)
-
- return keys.every(
- (key) => key === 'service' || key === 'action' || key === 'resourcePath'
- ) &&
- values.every(value => !!value)
-}
-
-/**
- * @description Is the krn resourcePath (if present) valid (doesn't contain any restricted characters)
- * @param {string} [resourcePath] The krnArg resource path
- * @return {*} {boolean}
- */
-
-export const krnResourcePathIsValid = (resourcePath) => {
- if (resourcePath.includes('}') || resourcePath.includes('{')) {
- // Log error to help developer find invalid array
- console.error(`Invalid krn resourcePath value: ${resourcePath}`)
-
- return false
- }
-
- return true
-}
diff --git a/src/router/index.ts b/src/router/index.ts
index e03caf1c..04823a22 100644
--- a/src/router/index.ts
+++ b/src/router/index.ts
@@ -87,10 +87,9 @@ export const portalRouter = () => {
name: 'spec',
meta: {
title: helpText.specTitle,
- isAuthorized: (route, { portalId }) => canUserAccess({
- service: 'konnect',
- action: '#view',
- resourcePath: `portals/${portalId}/services/${route.params.product}`
+ isAuthorized: (route) => canUserAccess({
+ action: 'view',
+ productId: route.params.product
})
},
component: () => import('../views/Spec.vue')
@@ -100,10 +99,9 @@ export const portalRouter = () => {
name: 'api-documentation-page',
meta: {
title: helpText.docsTitle,
- isAuthorized: (route, { portalId }) => canUserAccess({
- service: 'konnect',
- action: '#view',
- resourcePath: `portals/${portalId}/services/${route.params.product}`
+ isAuthorized: (route) => canUserAccess({
+ action: 'view',
+ productId: route.params.product
})
},
component: () => import('../views/ApiDocumentationPage.vue')
diff --git a/src/router/route-utils.ts b/src/router/route-utils.ts
index 8ef25bc3..09a0ee80 100644
--- a/src/router/route-utils.ts
+++ b/src/router/route-utils.ts
@@ -1,5 +1,5 @@
import useLDFeatureFlag from '@/hooks/useLDFeatureFlag'
-import { usePermissionsStore } from '@/stores'
+import { ProductAction, usePermissionsStore } from '@/stores'
export const AUTH_ROUTES = {
login: true,
@@ -18,7 +18,7 @@ export const PRIVATE_ROUTES = {
...AUTH_ROUTES
}
-export function canUserAccess (krnArgs) {
+export function canUserAccess (krnArgs: { action: ProductAction; productId: string }) {
const { canUserAccess } = usePermissionsStore()
return canUserAccess(krnArgs)
diff --git a/src/services/SessionCookie.ts b/src/services/SessionCookie.ts
index d16336f1..b42f723c 100644
--- a/src/services/SessionCookie.ts
+++ b/src/services/SessionCookie.ts
@@ -1,6 +1,4 @@
import { authApi } from '@/services'
-import { usePermissionsStore, useAppStore } from '@/stores'
-import { storeToRefs } from 'pinia'
/**
* @typedef {Object} SessionUser
@@ -76,10 +74,6 @@ export default class SessionCookie {
}
async saveData (data: Record, force = true) {
- const appStore = useAppStore()
- const permissionsStore = usePermissionsStore()
- const { portalId, isRbacEnabled, isPublic } = storeToRefs(appStore)
-
this.data = data
const sessionExists = this.exists()
@@ -88,25 +82,6 @@ export default class SessionCookie {
if (force || (!force && !sessionExists)) {
localStorage.setItem(this.sessionName, this.encode(this.data))
}
-
- if (sessionExists && !isPublic.value && isRbacEnabled.value) {
- try {
- const { data: developerPermissions } = await authApi.client.get(`/api/v2/portals/${portalId.value}/developers/me/permissions`)
-
- // response can be a JSON (object) or string
- // when permissions feature flag is not enabled, string with HTTP 200 is returned
- if (typeof developerPermissions === 'object') {
- // Add permission krns to the store
- await permissionsStore.addKrns({
- krns: developerPermissions,
- replaceAll: true
- })
- }
- } catch (e) {
- // eslint-disable-next-line no-console
- console.error('Failed to fetch permissions', e)
- }
- }
}
getUser () {
diff --git a/src/stores/permissions.ts b/src/stores/permissions.ts
index b83166f1..4ddbb9f9 100644
--- a/src/stores/permissions.ts
+++ b/src/stores/permissions.ts
@@ -1,134 +1,28 @@
import { defineStore, storeToRefs } from 'pinia'
-import { ref } from 'vue'
-import { resourcePathMatches, krnArgIsValid } from '@/helpers/permissions'
import { useAppStore } from '@/stores'
+import { portalApiV2 } from '@/services'
+import { ProductActionsResponseActions } from '@kong/sdk-portal-js'
-const KRN_STRUCT = {
- prefix: 'krn',
- regionBlockPrefix: 'reg/',
- orgBlockPrefix: 'org/',
- blockDelimeter: ':',
- pathDelimeter: '/'
-}
+export type ProductAction = keyof ProductActionsResponseActions
export const usePermissionsStore = defineStore('permissions', () => {
- const krns = ref([])
-
- const addKrns = (payload) => {
- // If replaceAll is true, first clear all stored krns
- if (payload.replaceAll === true) {
- krns.value = []
- }
-
- // Store new krns
- krns.value = [...new Set([...krns.value, ...payload.krns].map(krn => JSON.stringify(krn)))].map(krn => JSON.parse(krn))
- }
-
- const parseKrn = (krnResource) => {
- const parsedKrn = {
- service: null,
- region: null,
- organization: null,
- resourcePath: null
- }
-
- // If not a valid krn, exit early
- if (!krnResource.startsWith(`${KRN_STRUCT.prefix}${KRN_STRUCT.blockDelimeter}`)) {
- console.error('parseKrn: Invalid KRN prefix')
-
- return null
- }
-
- const krnBlocks = krnResource.split(KRN_STRUCT.blockDelimeter)
-
- const [serviceBlock, regionBlock, orgBlock, resourceBlock] = krnBlocks.slice(1)
-
- // length of 4 gives: krn block, service block, geo block, and org block
- if (krnBlocks.length < 4) {
- console.error('parseKrn: Invalid number of KRN blocks')
-
- return null
- }
-
- parsedKrn.service = serviceBlock
- parsedKrn.region = regionBlock.replace(KRN_STRUCT.regionBlockPrefix, '')
- parsedKrn.organization = orgBlock.replace(KRN_STRUCT.orgBlockPrefix, '')
-
- if (resourceBlock !== undefined) {
- parsedKrn.resourcePath = resourceBlock
- }
-
- return parsedKrn
- }
-
- const canUserAccess = async (requestedPermission) => {
+ const canUserAccess = async (requestedPermission: { action: ProductAction; productId: string }) => {
const appStore = useAppStore()
- const { portalId, isRbacEnabled, isPublic } = storeToRefs(appStore)
+ const { isRbacEnabled, isPublic } = storeToRefs(appStore)
if (isPublic.value || !isRbacEnabled.value) {
return true
}
- // If object is invalid KRN exist early
- if (!krnArgIsValid(requestedPermission)) {
- return false
- }
-
- let requestedResourcePath
-
- const { service: requestedService, action, resourcePath } = requestedPermission
-
- // Block below is to simplify usage of `canUserAccess` by allowing to omit
- // first part which is static and contains always `portals/`
- if (resourcePath.startsWith('portals')) {
- // full path provided, omit adding prefix `portals/`
- requestedResourcePath = resourcePath
- } else {
- // simplify path - adding prefix `portals/`
- requestedResourcePath = `portals/${portalId.value}/${resourcePath}`
- }
-
- // If set, ensure the requestedResourcePath does not include an `undefined` string
- // An `undefined` string is expected when evaluating the route guards in some scenarios as
- // provided route params will not be set at initial evaluation (e.g. when the Sidebar evaluates
- // permissions on app hydration); however, the params _will_ be present and valid in the beforeEach hook.
- if (requestedResourcePath.includes('/undefined')) {
- // Return false since this is an invalid resourcePath
- return false
- }
-
- // Ensure action starts with a hash '#'
- const requestedAction = action && action.startsWith('#') ? action : `#${action}`
-
- // Check if any krns exist in the store that match the requestedService and requestedResourcePath
- let matchingResources = krns.value.filter((krn) => {
- const parsedKrn = parseKrn(krn.resource)
-
- // If the requested service does not equal the krn service, exit early
- if (requestedService !== parsedKrn.service) {
- return false
- }
-
- return resourcePathMatches(parsedKrn, requestedResourcePath)
- })
-
- // If the requestedService is defined and the user has no matching resource paths
- // we return false as we fetch all of permissions for now and we do not need to refetch
- // permissions if requested one is not a part of ours.
- // Might change in the fututre
- if (matchingResources.length === 0) {
- return false
- }
+ const { action, productId } = requestedPermission
- // Filter the resources with matching resourcePath by the requested action(s)
- matchingResources = matchingResources.filter((krn) => krn.actions.some(action => !!requestedAction && requestedAction === action))
+ const { data } = await portalApiV2.service.productsApi.getProductActions({ productId })
- // Return true if the matchingResources contains an allowed action
- return matchingResources.length > 0
+ // make sure the requested action is true on the response
+ return data.actions[action]
}
return {
- addKrns,
canUserAccess
}
})
diff --git a/src/views/Spec.vue b/src/views/Spec.vue
index 814779d0..56ff44d3 100644
--- a/src/views/Spec.vue
+++ b/src/views/Spec.vue
@@ -115,7 +115,7 @@ export default defineComponent({
]
const applicationRegistrationEnabled = computed(() => {
- return currentVersion.value.registration_configs?.length && isAllowedToRegister.value
+ return Boolean(currentVersion.value.registration_configs?.length && isAllowedToRegister.value)
})
const helpText = useI18nStore().state.helpText
@@ -161,11 +161,14 @@ export default defineComponent({
}
})
- watch(() => props.product, async () => {
+ watch(() => props.product, async (newProduct, oldProduct) => {
+ if (newProduct?.id === oldProduct?.id) {
+ return
+ }
+
isAllowedToRegister.value = await canUserAccess({
- service: 'konnect',
- action: '#consume',
- resourcePath: `services/${$route.params.product}`
+ action: 'register',
+ productId: $route.params.product.toString()
})
await processProduct()
@@ -186,12 +189,11 @@ export default defineComponent({
}
})
- watch(() => $route.params.product_version, async (productVersionId) => {
- if (productVersionId) {
+ watch(() => $route.params.product_version, async (productVersionId, oldValue) => {
+ if (productVersionId && (oldValue !== productVersionId)) {
isAllowedToRegister.value = await canUserAccess({
- service: 'konnect',
- action: '#consume',
- resourcePath: `services/${$route.params.product}`
+ action: 'register',
+ productId: $route.params.product.toString()
})
// this is not called on page load, but will be called when back button clicked and on select
@@ -202,9 +204,8 @@ export default defineComponent({
onMounted(async () => {
isAllowedToRegister.value = await canUserAccess({
- service: 'konnect',
- action: '#consume',
- resourcePath: `services/${$route.params.product}`
+ action: 'register',
+ productId: $route.params.product.toString()
})
await processProduct()
diff --git a/yarn.lock b/yarn.lock
index e74ea6be..e8500c6d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1045,7 +1045,23 @@
vue-recaptcha "^2.0.3"
xstate "^4.38.2"
-"@kong/kongponents@8.123.3", "@kong/kongponents@^8.32.0":
+"@kong/kongponents@8.126.1":
+ version "8.126.1"
+ resolved "https://registry.yarnpkg.com/@kong/kongponents/-/kongponents-8.126.1.tgz#f5f68e534a38df68c21c231c2e8ff728fbd2bd07"
+ integrity sha512-8GdHy+/pcALzabuTplnZZSsAnb8rBZhaMAx4nADfqrt6uPvfDHI6AQfLZg6Fc+D3CPjo9q2PL+o1kf2a5nuYLg==
+ dependencies:
+ date-fns "^2.30.0"
+ date-fns-tz "^2.0.0"
+ focus-trap "^7.5.2"
+ focus-trap-vue "^4.0.2"
+ popper.js "^1.16.1"
+ sortablejs "^1.15.0"
+ swrv "^1.0.4"
+ uuid "^9.0.0"
+ v-calendar "3.0.0-alpha.8"
+ vue-draggable-next "^2.2.1"
+
+"@kong/kongponents@^8.32.0":
version "8.123.3"
resolved "https://registry.yarnpkg.com/@kong/kongponents/-/kongponents-8.123.3.tgz#038f5019f958d4a5da0db57a7b84dc1ab90b8212"
integrity sha512-nE0q69Imr5C5Q3rOW5mu+5h18Pi8WmnA1+s+yZNHlC5xvy3zSPo/xbpTfBqkk/VmQiu2oXqtQMMmxgtFoKF0Tg==
@@ -1407,6 +1423,11 @@
"@pnpm/network.ca-file" "^1.0.1"
config-chain "^1.1.11"
+"@popperjs/core@2.4.0":
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.4.0.tgz#0e1bdf8d021e7ea58affade33d9d607e11365915"
+ integrity sha512-NMrDy6EWh9TPdSRiHmHH2ye1v5U0gBD7pRYwSwJvomx7Bm4GG04vu63dYiVzebLOx2obPpJugew06xVP0Nk7hA==
+
"@semantic-release/changelog@6.0.3":
version "6.0.3"
resolved "https://registry.yarnpkg.com/@semantic-release/changelog/-/changelog-6.0.3.tgz#6195630ecbeccad174461de727d5f975abc23eeb"
@@ -8953,7 +8974,7 @@ pluralize@^8.0.0:
resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1"
integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==
-popper.js@^1.15.0:
+popper.js@^1.15.0, popper.js@^1.16.1:
version "1.16.1"
resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.1.tgz#2a223cb3dc7b6213d740e40372be40de43e65b1b"
integrity sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==
@@ -11383,6 +11404,17 @@ uuid@^9.0.0, uuid@^9.0.1:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30"
integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==
+v-calendar@3.0.0-alpha.8:
+ version "3.0.0-alpha.8"
+ resolved "https://registry.yarnpkg.com/v-calendar/-/v-calendar-3.0.0-alpha.8.tgz#3bc8c69f4788fb527c39706f41fd2a502a17c827"
+ integrity sha512-T23H5UbK0EomrwArlF/jrT2LFbV/lu+Bp9JroZ1paN6rPoaMyvE+HrLxvAmUgi+pODrdTURDMzM3+WPgeFKEBQ==
+ dependencies:
+ "@popperjs/core" "2.4.0"
+ "@types/lodash" "^4.14.165"
+ date-fns "^2.16.1"
+ date-fns-tz "^1.0.12"
+ lodash "^4.17.20"
+
v-calendar@^3.0.0-alpha.8:
version "3.0.3"
resolved "https://registry.yarnpkg.com/v-calendar/-/v-calendar-3.0.3.tgz#f04c625d3c3352d5685099bb4ad3e24d0530a7d4"
@@ -11468,6 +11500,11 @@ vue-draggable-next@^2.1.1:
resolved "https://registry.yarnpkg.com/vue-draggable-next/-/vue-draggable-next-2.2.0.tgz#cdefc345f950d64afb013436bf3111c7d87239f3"
integrity sha512-JQ7Ac4knnpsA47/acUhRR7negDHNZDLdpbXRR+n89f516rJDt+eHh48tfqTe80q2UfnLymQ46zi81gCMKFU4DQ==
+vue-draggable-next@^2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/vue-draggable-next/-/vue-draggable-next-2.2.1.tgz#adbe98c74610cca8f4eb63f92042681f96920451"
+ integrity sha512-EAMS1IRHF0kZO0o5PMOinsQsXIqsrKT1hKmbICxG3UEtn7zLFkLxlAtajcCcUTisNvQ6TtCB5COjD9a1raNADw==
+
vue-eslint-parser@^9.3.1:
version "9.3.1"
resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-9.3.1.tgz#429955e041ae5371df5f9e37ebc29ba046496182"