Pulling NPM artifact provided by Content Selector results in 403 error

Overview

Similar to my other post, I am using Content Selectors to provide filtered access for Company A to Company B’s hosted repositories. While in the previous post, I was able to get Maven builds to work properly, I am now running into an issue with NPM builds. When attempting to install an artifact, it results in a 403 error (forbidden). The Content Selector provides read+browse access to Company A users to the Company B’s companyB-npm-hosted repository and, for the time being, provides access to everything in that repository. The only solution to get this working is to circumvent the Content Selector altogether and provide nx-repository-view-npm-companyB-npm-hosted-read privilege (not ideal).

The error:

403 Forbidden - GET https://my.company.com/repository/companyB-npm-hosted/@companyB%2fnpm-dep-1
In most cases, you or one of your dependencies are requesting
a package version that is forbidden by your security policy, or
on a server you do not have access to.

Desired Result

When using a Content Selector to expose only certain artifacts in a repository, using npm install @scoped/artifact should complete successfully assuming the dependencies of that @scoped/artifact are also provided either in the Content Selector or from a proxy repository which the user can see.

Repositories

I’ll provide a general overview of the current layout of the repositories and the artifact dependencies so that the problem can be better understood.

Firstly, the repositories in question. Company A has the following NPM repositories:

companyA-npm-group
- companyA-npm-hosted
- global-npm-proxy (shared amongst all companies)

Company B has the following NPM repositories:

companyB-npm-group
- companyB-npm-hosted
- global-npm-proxy (shared amongst all companies)

Content

Considering this example, I’ll provide the contents of Company B’s companyB-npm-hosted repository and the dependencies in question. I’ll also provide where these dependencies live. Finally, I’ll provide the RBAC privileges/roles applied to Company A users to provide the full picture.

Firstly, the contents of Company B’s companyB-npm-hosted repository:

@companyB
- npm-dep-1
  - { versions of npm-dep-1 }
- npm-dep-2
  - { versions of npm-dep-2 }
- npm-dep-3
  - { versions of npm-dep-3 }

The artifact that is being installed is npm-dep-1 which has a dependency graph as follows:

@angular/common@12.2.0 (exists in `global-npm-proxy`)
@angular/core@12.2.0 (exists in `global-npm-proxy`)
@companyB/npm-dep-2
@companyB/npm-dep-3

Now for the RBAC privileges and roles in question. Company A users have the following RBAC setup:

companyA-role:
- nx-component-upload
- nx-search-read
- companyA-read-npm
- companyA-write-npm
- companyA-read-npm-companyB

companyA-read-npm:
- nx-repository-view-npm-companyA-npm-group-read
- nx-repository-view-npm-companyA-npm-group-browse

companyA-write-npm:
- nx-repository-view-npm-companyA-npm-hosted-*

companyA-read-npm-companyB:
- read-companyB-public-deps (this is the Content Selector privilege)

The Content Selector read-companyB-public-deps has the following expression:

(format == "npm") and 
(path =~ "/|/@companyB/|/@companyB/npm-dep-1/.*" or 
path =~ "/|/@companyB/|/@companyB/npm-dep-2/.*" or 
path =~ "/|/@companyB/|/@companyB/npm-dep-3/.*")

The Content Selector is applied to the companyB-npm-hosted repository and provides read+browse actions.

As you can see, I’ve provided the path to all of the dependencies within companyB-npm-hosted via the Content Selector. However, when I attempt to install @companyB/npm-dep-1 the error mentioned earlier continues to prevail. As mentioned, I was able to “alleviate” this problem by completely circumventing the Content Selector by giving all Company A users the ability to read from companyB-npm-hosted entirely via the nx-repository-view-npm-companyB-npm-hosted-read privilege. Because this alternative privilege fixes the issue, this leads me to believe that the issue lies with resolving the dependency tree during installation. Company A users can see the companyB-npm-hosted repository and its contents. However, during installation it is failing to pull the dependencies of @companyB/npm-dep-1 which includes other artifacts from the companyB-npm-hosted repository. Are Content Selectors applied for each and every attempt to pull from a repository in Nexus? It seems to me that the Content Selector is not being applied when attempting to resolve dependencies of a particular artifact.

The log that outlines the resolution of npm install @companyB/npm-dep-1 is as follows:

silly fetch manifest @companyB/npm-dep-1@*
http fetch GET 403 https://my.company.com/repository/companyB-npm-hosted/@companyB%2fnpm-dep-1 239ms (cache skip)
silly placeDep ROOT @companyB/npm-dep-1@ OK for:  want: *
timing idealTree:#root Completed in 247ms
timing idealTree:node_modules/@companyB/npm-dep-1 Completed in 0ms
timing idealTree:buildDeps Completed in 247ms
timing idealTree:fixDepFlags Completed in 0ms
timing idealTree Completed in 252ms
timing command:install Completed in 255ms
verbose stack HttpErrorGeneral: 403 Forbidden - GET https://my.company.com/repository/companyB-npm-hosted/@companyB%2fnpm-dep-1
verbose stack     at /opt/homebrew/lib/node_modules/npm/node_modules/npm-registry-fetch/lib/check-response.js:93:15
verbose stack     at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
verbose stack     at async [nodeFromEdge] (/opt/homebrew/lib/node_modules/npm/node_modules/@npmcli/arborist/lib/arborist/build-ideal-tree.js:1101:19)
verbose stack     at async [buildDepStep] (/opt/homebrew/lib/node_modules/npm/node_modules/@npmcli/arborist/lib/arborist/build-ideal-tree.js:970:11)
verbose stack     at async Arborist.buildIdealTree (/opt/homebrew/lib/node_modules/npm/node_modules/@npmcli/arborist/lib/arborist/build-ideal-tree.js:216:7)
verbose stack     at async Promise.all (index 1)
verbose stack     at async Arborist.reify (/opt/homebrew/lib/node_modules/npm/node_modules/@npmcli/arborist/lib/arborist/reify.js:153:5)
verbose stack     at async Install.exec (/opt/homebrew/lib/node_modules/npm/lib/commands/install.js:159:5)
verbose stack     at async module.exports (/opt/homebrew/lib/node_modules/npm/lib/cli.js:78:5)
verbose statusCode 403
verbose pkgid @companyB/npm-dep-1@*
verbose cwd /Users/{ my user directory }
verbose Darwin 21.4.0
verbose node v18.0.0
verbose npm  v8.6.0
error code E403
error 403 403 Forbidden - GET https://my.company.com/repository/companyB-npm-hosted/@companyB%2fnpm-dep-1
error 403 In most cases, you or one of your dependencies are requesting
error 403 a package version that is forbidden by your security policy, or
error 403 on a server you do not have access to.

The .npmrc file used to install this artifact is as follows:

registry=https://my.company.com/repository/companyA-npm-group
@companyB:registry=https://my.company.com/repository/companyB-npm-hosted
//my.company.com/repository/companyA-npm-group:_auth={ user:pass base64 encoded }
//my.company.com/repository/companyB-npm-hosted:_auth={ user:pass base64 encoded }

The user:password that is encoded in this .npmrc file can see the contents of the companyB-npm-hosted repository thanks to the Content Selector. I’m making sure that NPM knows about both repositories so that the proxy artifacts (like @angular/core) can be discovered through the global-npm-proxy that is part of each Company’s group repository.

Any help would be much appreciated. Thank you.

http fetch GET 403 https://my.company.com/repository/companyB-npm-hosted/@companyB%2Fnpm-dep-1 239ms (cache skip)

There’s no trailing slash at the end of that URL, so it isn’t covered by the content selector. The content selector as written will only match the above URL if it ends in a trailing slash.

I’m not sure I follow. Wouldn’t the the following expression cover that specific case:
/|/@companyB/|/@companyB/npm-dep-1/.*? As I understand it, the expression drills down to the npm-dep-1 directory and then says “anything inside”. Would I need to include the formatted / (i.e. %2f) in the expression instead? Something like this:
/|/@companyB/|/@companyB%2fnpm-dep-1/.*

Okay, I believe I may have gotten it working. It’s strange that this isn’t synonymous with Maven. Nonetheless, I was able to get it working by modifying the content selector expression from:
/|/@companyB/|/@companyB/npm-dep-1/.*
to:
/|/@companyB/|/@companyB/npm-dep-1.*

I removed the final / in the expression before the catch-all and it was able to download.