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

sonatype nexus - quirks modes #782

Merged
merged 24 commits into from
May 29, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/reference/ocm_credential-handling.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ The following credential consumer types are used/supported:
- <code>password</code>: the basic auth password


- <code>NpmRegistry</code>: NPM repository
- <code>NpmRegistry</code>: NPM registry

It matches the <code>NpmRegistry</code> consumer type and additionally acts like
the <code>hostpath</code> type.
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/ocm_get_credentials.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ Matchers exist for the following usage contexts or consumer types:
- <code>password</code>: the basic auth password


- <code>NpmRegistry</code>: NPM repository
- <code>NpmRegistry</code>: NPM registry

It matches the <code>NpmRegistry</code> consumer type and additionally acts like
the <code>hostpath</code> type.
Expand Down
1 change: 1 addition & 0 deletions docs/reference/ocm_logging.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ The following *realms* are used by the command line tool:
- <code>ocm/compdesc</code>: component descriptor handling
- <code>ocm/config</code>: configuration management
- <code>ocm/context</code>: context lifecycle
- <code>ocm/credentials</code>: Credentials
- <code>ocm/credentials/dockerconfig</code>: docker config handling as credential repository
- <code>ocm/credentials/vault</code>: HashiCorp Vault Access
- <code>ocm/downloader</code>: Downloaders
Expand Down
39 changes: 20 additions & 19 deletions pkg/contexts/credentials/builtin/npm/identity/identity.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
package identity

import (
"path"
"net/url"

. "net/url"

"github.com/open-component-model/ocm/pkg/common"
"github.com/open-component-model/ocm/pkg/contexts/credentials/cpi"
"github.com/open-component-model/ocm/pkg/contexts/credentials/identity/hostpath"
"github.com/open-component-model/ocm/pkg/listformat"
Expand Down Expand Up @@ -37,31 +34,35 @@ func init() {
ATTR_TOKEN, "the token attribute. May exist after login at any npm registry. Check your .npmrc file!",
})

cpi.RegisterStandardIdentity(CONSUMER_TYPE, hostpath.IdentityMatcher(CONSUMER_TYPE), `NPM repository
cpi.RegisterStandardIdentity(CONSUMER_TYPE, hostpath.IdentityMatcher(CONSUMER_TYPE), `NPM registry

It matches the <code>`+CONSUMER_TYPE+`</code> consumer type and additionally acts like
the <code>`+hostpath.IDENTITY_TYPE+`</code> type.`,
attrs)
}

func GetConsumerId(rawURL string, pkgName string) cpi.ConsumerIdentity {
url, err := Parse(rawURL)
var identityMatcher = hostpath.IdentityMatcher(CONSUMER_TYPE)

func IdentityMatcher(pattern, cur, id cpi.ConsumerIdentity) bool {
return identityMatcher(pattern, cur, id)
}
Skarlso marked this conversation as resolved.
Show resolved Hide resolved

func GetConsumerId(rawURL, groupId string) (cpi.ConsumerIdentity, error) {
_url, err := url.JoinPath(rawURL, groupId)
if err != nil {
return nil
return nil, err
}

url.Path = path.Join(url.Path, pkgName)
return hostpath.GetConsumerIdentity(CONSUMER_TYPE, url.String())
return hostpath.GetConsumerIdentity(CONSUMER_TYPE, _url), nil
}

func GetCredentials(ctx cpi.ContextProvider, repoUrl string, pkgName string) common.Properties {
id := GetConsumerId(repoUrl, pkgName)
if id == nil {
return nil
func GetCredentials(ctx cpi.ContextProvider, repoUrl string, pkgName string) (cpi.Credentials, error) {
id, err := GetConsumerId(repoUrl, pkgName)
if err != nil {
return nil, err
}
credentials, err := cpi.CredentialsForConsumer(ctx.CredentialsContext(), id)
if credentials == nil || err != nil {
return nil
if id == nil {
logging.DynamicLogger(REALM).Debug("No consumer identity found.", "url", repoUrl, "groupId", pkgName)
return nil, nil
}
return credentials.Properties()
return cpi.CredentialsForConsumer(ctx.CredentialsContext(), id)
}
6 changes: 3 additions & 3 deletions pkg/contexts/credentials/repositories/npm/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ package npm_test
import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
. "github.com/open-component-model/ocm/pkg/testutils"

"github.com/open-component-model/ocm/pkg/common"
"github.com/open-component-model/ocm/pkg/contexts/credentials"
"github.com/open-component-model/ocm/pkg/contexts/credentials/builtin/npm/identity"
"github.com/open-component-model/ocm/pkg/contexts/credentials/repositories/npm"
. "github.com/open-component-model/ocm/pkg/testutils"
)

var _ = Describe("Config deserialization Test Environment", func() {
Expand All @@ -26,8 +26,8 @@ var _ = Describe("Config deserialization Test Environment", func() {
spec := npm.NewRepositorySpec("testdata/.npmrc")

_ = Must(ctx.RepositoryForSpec(spec))
id := identity.GetConsumerId("registry.npmjs.org", "pkg")

id, err := identity.GetConsumerId("registry.npmjs.org", "pkg")
Expect(err).To(BeNil())
mandelsoft marked this conversation as resolved.
Show resolved Hide resolved
creds := Must(credentials.CredentialsForConsumer(ctx, id))
Expect(creds).NotTo(BeNil())
Expect(creds.GetProperty(identity.ATTR_TOKEN)).To(Equal("npm_TOKEN"))
Expand Down
8 changes: 6 additions & 2 deletions pkg/contexts/credentials/repositories/npm/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,12 @@ func (p *ConsumerProvider) get(requested cpi.ConsumerIdentity, currentFound cpi.
var creds cpi.CredentialsSource

for key, value := range all {
id := npm.GetConsumerId("https://"+key, "")

id, err := npm.GetConsumerId("https://"+key, "")
if err != nil {
log := logging.Context().Logger(npm.REALM)
log.LogError(err, "Failed to get consumer id", "key", key, "value", value)
return nil, nil
}
if m(requested, currentFound, id) {
creds = newCredentials(value)
currentFound = id
Expand Down
2 changes: 1 addition & 1 deletion pkg/contexts/credentials/repositories/npm/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func (r *Repository) Read(force bool) error {
}

if r.path == "" {
return fmt.Errorf("npmrc path not provided")
return errors.New("npmrc path not provided")
}
cfg, path, err := readNpmConfigFile(r.path)
if err != nil {
Expand Down
88 changes: 65 additions & 23 deletions pkg/contexts/ocm/accessmethods/npm/method.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ func (a *AccessSpec) AccessMethod(c accspeccpi.ComponentVersionAccess) (accspecc
}

func (a *AccessSpec) GetInexpensiveContentVersionIdentity(access accspeccpi.ComponentVersionAccess) string {
meta, err := a.getPackageMeta(access.GetContext())
meta, err := a.GetPackageVersion(access.GetContext())
if err != nil {
return ""
}
Expand All @@ -96,43 +96,70 @@ func (a *AccessSpec) GetInexpensiveContentVersionIdentity(access accspeccpi.Comp
return ""
}

// PackageUrl returns the URL of the NPM package (Registry/Package/Version).
// PackageUrl returns the URL of the NPM package (Registry/Package).
func (a *AccessSpec) PackageUrl() string {
return a.Registry + path.Join("/", a.Package)
}

// PackageVersionUrl returns the URL of the NPM package-version (Registry/Package/Version).
func (a *AccessSpec) PackageVersionUrl() string {
return a.Registry + path.Join("/", a.Package, a.Version)
}
mandelsoft marked this conversation as resolved.
Show resolved Hide resolved

func (a *AccessSpec) getPackageMeta(ctx accspeccpi.Context) (*meta, error) {
func (a *AccessSpec) GetPackageVersion(ctx accspeccpi.Context) (*npm.Version, error) {
r, err := reader(a, vfsattr.Get(ctx), ctx)
if err != nil {
return nil, err
}
buf := &bytes.Buffer{}
_, err = io.Copy(buf, io.LimitReader(r, 200000))
defer r.Close()
buf, err := io.ReadAll(r)
if err != nil {
return nil, errors.Wrapf(err, "cannot get version metadata for %s", a.PackageUrl())
return nil, errors.Wrapf(err, "cannot get version metadata for %s", a.PackageVersionUrl())
}

var metadata meta

err = json.Unmarshal(buf.Bytes(), &metadata)
if err != nil {
return nil, errors.Wrapf(err, "cannot unmarshal version metadata for %s", a.PackageUrl())
var version npm.Version
err = json.Unmarshal(buf, &version)
if err != nil || version.Dist.Tarball == "" {
// ugly fallback as workaround for https://github.com/sonatype/nexus-public/issues/224
var project npm.Project
err = json.Unmarshal(buf, &project) // parse the complete project
if err != nil {
return nil, errors.Wrapf(err, "cannot unmarshal version metadata for %s", a.PackageVersionUrl())
}
v, ok := project.Version[a.Version] // and pick only the specified version
if !ok {
return nil, errors.Newf("version '%s' doesn't exist", a.Version)
}
version = v
}
return &metadata, nil
return &version, nil
}

////////////////////////////////////////////////////////////////////////////////

func newMethod(c accspeccpi.ComponentVersionAccess, a *AccessSpec) (accspeccpi.AccessMethodImpl, error) {
factory := func() (blobaccess.BlobAccess, error) {
meta, err := a.getPackageMeta(c.GetContext())
meta, err := a.GetPackageVersion(c.GetContext())
if err != nil {
return nil, err
}

f := func() (io.ReadCloser, error) {
return reader(a, vfsattr.Get(c.GetContext()), c.GetContext(), meta.Dist.Tarball)
}
if meta.Dist.Integrity != "" {
tf := f
f = func() (io.ReadCloser, error) {
r, err := tf()
if err != nil {
return nil, err
}
digest, err := iotools.DecodeBase64ToHex(meta.Dist.Integrity)
if err != nil {
return nil, err
}
return iotools.VerifyingReaderWithHash(r, crypto.SHA512, digest), nil
}
}
if meta.Dist.Shasum != "" {
tf := f
f = func() (io.ReadCloser, error) {
Expand All @@ -149,15 +176,8 @@ func newMethod(c accspeccpi.ComponentVersionAccess, a *AccessSpec) (accspeccpi.A
return accspeccpi.NewDefaultMethodImpl(c, a, "", mime.MIME_TGZ, factory), nil
}

type meta struct {
Dist struct {
Shasum string `json:"shasum"`
Tarball string `json:"tarball"`
} `json:"dist"`
}

func reader(a *AccessSpec, fs vfs.FileSystem, ctx cpi.ContextProvider, tar ...string) (io.ReadCloser, error) {
url := a.PackageUrl()
url := a.PackageVersionUrl()
if len(tar) > 0 {
url = tar[0]
}
Expand All @@ -170,12 +190,34 @@ func reader(a *AccessSpec, fs vfs.FileSystem, ctx cpi.ContextProvider, tar ...st
if err != nil {
return nil, err
}
npm.Authorize(req, ctx, a.Registry, a.Package)
err = npm.BasicAuth(req, ctx, a.Registry, a.Package)
if err != nil {
return nil, err
}
c := &http.Client{}
resp, err := c.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode == http.StatusNotFound {
// maybe it's stupid Nexus - https://github.com/sonatype/nexus-public/issues/224?
resp.Body.Close()

url = a.PackageUrl()
req, err = http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil)
if err != nil {
return nil, err
}
err = npm.BasicAuth(req, ctx, a.Registry, a.Package)
if err != nil {
return nil, err
}
resp, err = c.Do(req)
if err != nil {
return nil, err
}
}

if resp.StatusCode != http.StatusOK {
defer resp.Body.Close()
buf := &bytes.Buffer{}
Expand Down
23 changes: 12 additions & 11 deletions pkg/contexts/ocm/blobhandler/handlers/generic/npm/blobhandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"net/http"
"net/url"

crds "github.com/open-component-model/ocm/pkg/contexts/credentials/cpi"
"github.com/open-component-model/ocm/pkg/contexts/ocm/accessmethods/npm"
"github.com/open-component-model/ocm/pkg/contexts/ocm/cpi"
"github.com/open-component-model/ocm/pkg/logging"
Expand Down Expand Up @@ -64,14 +65,8 @@ func (b *artifactHandler) StoreBlob(blob cpi.BlobAccess, _ string, _ string, _ c
log = log.WithValues("package", pkg.Name, "version", pkg.Version)
log.Debug("identified")

token, err := npmLogin.BearerToken(ctx.GetContext(), b.spec.Url, pkg.Name)
if err != nil {
// we assume, it's not possible to publish anonymous - without token
return nil, err
}

// check if package exists
exists, err := packageExists(b.spec.Url, *pkg, token)
exists, err := packageExists(b.spec.Url, *pkg, ctx.GetContext())
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -104,8 +99,11 @@ func (b *artifactHandler) StoreBlob(blob cpi.BlobAccess, _ string, _ string, _ c
if err != nil {
return nil, err
}
req.Header.Set("authorization", "Bearer "+token)
req.Header.Set("content-type", "application/json")
err = npmLogin.Authorize(req, ctx.GetContext(), b.spec.Url, pkg.Name)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")

// send PUT request - upload tgz
client := http.Client{}
Expand All @@ -127,13 +125,16 @@ func (b *artifactHandler) StoreBlob(blob cpi.BlobAccess, _ string, _ string, _ c
}

// Check if package already exists in npm registry. If it does, checks if it's the same.
func packageExists(repoUrl string, pkg Package, token string) (bool, error) {
func packageExists(repoUrl string, pkg Package, ctx crds.ContextProvider) (bool, error) {
client := http.Client{}
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, repoUrl+"/"+url.PathEscape(pkg.Name)+"/"+url.PathEscape(pkg.Version), nil)
if err != nil {
return false, err
}
req.Header.Set("authorization", "Bearer "+token)
err = npmLogin.Authorize(req, ctx, repoUrl, pkg.Name)
if err != nil {
return false, err
}
resp, err := client.Do(req)
if err != nil {
return false, err
Expand Down
21 changes: 21 additions & 0 deletions pkg/iotools/digests.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package iotools

import (
"encoding/base64"
"encoding/hex"
"regexp"
)

// Regular expression matching: e.g. 'sha512-', 'SHA-1:', 'Sha-256:', 'sHA42-',.
var re = regexp.MustCompile(`(?i)^sha(\d+-|\-\d+:)`)

// DecodeBase64ToHex decodes a base64 encoded string and returns the hex representation.
// Any prefix like 'sha512-' or 'SHA-256:' or 'Sha1-' is removed.
func DecodeBase64ToHex(b64encoded string) (string, error) {
b64encoded = re.ReplaceAllString(b64encoded, "")
digest, err := base64.StdEncoding.DecodeString(b64encoded)
if err != nil {
return "", err
}
return hex.EncodeToString(digest), nil
}
Loading
Loading