From 4473dacca406e4c84c0ac5e6e14393c659384afc Mon Sep 17 00:00:00 2001 From: Hilmar Falkenberg Date: Mon, 6 May 2024 10:30:10 +0200 Subject: [PATCH] Npm/auth access (#757) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description When credentials are configured, let's also use them for the read access. ## What type of PR is this? (check all applicable) - [x] 🍕 Feature - [ ] 🎇 Restructuring - [x] 🐛 Bug Fix - [ ] 📝 Documentation Update - [ ] 🎨 Style - [x] 🧑‍💻 Code Refactor - [ ] 🔥 Performance Improvements - [ ] ✅ Test - [ ] 🤖 Build - [ ] 🔁 CI - [ ] 📦 Chore (Release) - [ ] ⏩ Revert ## Related Tickets & Documents - Closes #753 ## Added tests? - [ ] 👍 yes - [ ] 🙅 no, because they aren't needed - [ ] 🙋 no, because I need help - [ ] Separate ticket for tests # (issue/pr) Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration ## Added to documentation? - [ ] 📜 README.md - [ ] 🙅 no documentation needed ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] Any dependent changes have been merged and published in downstream modules --- pkg/contexts/ocm/accessmethods/npm/method.go | 30 +++--- .../handlers/generic/npm/blobhandler.go | 35 ++----- .../handlers/generic/npm/publish.go | 41 -------- pkg/npm/login.go | 99 +++++++++++++++++++ 4 files changed, 123 insertions(+), 82 deletions(-) create mode 100644 pkg/npm/login.go diff --git a/pkg/contexts/ocm/accessmethods/npm/method.go b/pkg/contexts/ocm/accessmethods/npm/method.go index 7698cd9920..07784f25e9 100644 --- a/pkg/contexts/ocm/accessmethods/npm/method.go +++ b/pkg/contexts/ocm/accessmethods/npm/method.go @@ -1,7 +1,3 @@ -// SPDX-FileCopyrightText: 2022 SAP SE or an SAP affiliate company and Open Component Model contributors. -// -// SPDX-License-Identifier: Apache-2.0 - package npm import ( @@ -20,11 +16,13 @@ import ( "github.com/open-component-model/ocm/pkg/blobaccess" "github.com/open-component-model/ocm/pkg/common/accessio" "github.com/open-component-model/ocm/pkg/common/accessobj" + "github.com/open-component-model/ocm/pkg/contexts/credentials/cpi" "github.com/open-component-model/ocm/pkg/contexts/datacontext/attrs/vfsattr" "github.com/open-component-model/ocm/pkg/contexts/ocm/cpi/accspeccpi" "github.com/open-component-model/ocm/pkg/errors" "github.com/open-component-model/ocm/pkg/iotools" "github.com/open-component-model/ocm/pkg/mime" + "github.com/open-component-model/ocm/pkg/npm" "github.com/open-component-model/ocm/pkg/runtime" ) @@ -95,23 +93,27 @@ func (a *AccessSpec) GetInexpensiveContentVersionIdentity(access accspeccpi.Comp return "" } +// PackageUrl returns the URL of the NPM package (Registry/Package/Version). +func (a *AccessSpec) PackageUrl() string { + return a.Registry + path.Join("/", a.Package, a.Version) +} + func (a *AccessSpec) getPackageMeta(ctx accspeccpi.Context) (*meta, error) { - url := a.Registry + path.Join("/", a.Package, a.Version) - r, err := reader(url, vfsattr.Get(ctx)) + 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)) if err != nil { - return nil, errors.Wrapf(err, "cannot get version metadata for %s", url) + return nil, errors.Wrapf(err, "cannot get version metadata for %s", a.PackageUrl()) } var metadata meta err = json.Unmarshal(buf.Bytes(), &metadata) if err != nil { - return nil, errors.Wrapf(err, "cannot unmarshal version metadata for %s", url) + return nil, errors.Wrapf(err, "cannot unmarshal version metadata for %s", a.PackageUrl()) } return &metadata, nil } @@ -126,7 +128,7 @@ func newMethod(c accspeccpi.ComponentVersionAccess, a *AccessSpec) (accspeccpi.A } f := func() (io.ReadCloser, error) { - return reader(meta.Dist.Tarball, vfsattr.Get(c.GetContext())) + return reader(a, vfsattr.Get(c.GetContext()), c.GetContext(), meta.Dist.Tarball) } if meta.Dist.Shasum != "" { tf := f @@ -151,9 +153,11 @@ type meta struct { } `json:"dist"` } -func reader(url string, fs vfs.FileSystem) (io.ReadCloser, error) { - c := &http.Client{} - +func reader(a *AccessSpec, fs vfs.FileSystem, ctx cpi.ContextProvider, tar ...string) (io.ReadCloser, error) { + url := a.PackageUrl() + if len(tar) > 0 { + url = tar[0] + } if strings.HasPrefix(url, "file://") { path := url[7:] return fs.OpenFile(path, vfs.O_RDONLY, 0o600) @@ -163,6 +167,8 @@ func reader(url string, fs vfs.FileSystem) (io.ReadCloser, error) { if err != nil { return nil, err } + npm.Authorize(req, ctx, a.Registry, a.Package) + c := &http.Client{} resp, err := c.Do(req) if err != nil { return nil, err diff --git a/pkg/contexts/ocm/blobhandler/handlers/generic/npm/blobhandler.go b/pkg/contexts/ocm/blobhandler/handlers/generic/npm/blobhandler.go index 709141fd4d..76032ffca3 100644 --- a/pkg/contexts/ocm/blobhandler/handlers/generic/npm/blobhandler.go +++ b/pkg/contexts/ocm/blobhandler/handlers/generic/npm/blobhandler.go @@ -9,11 +9,11 @@ import ( "net/http" "net/url" - npmCredentials "github.com/open-component-model/ocm/pkg/contexts/credentials/builtin/npm/identity" "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" "github.com/open-component-model/ocm/pkg/mime" + npmLogin "github.com/open-component-model/ocm/pkg/npm" ) const BLOB_HANDLER_NAME = "ocm/npmPackage" @@ -52,7 +52,7 @@ func (b *artifactHandler) StoreBlob(blob cpi.BlobAccess, _ string, _ string, _ c } // read package.json from tarball to get name, version, etc. - log := logging.Context().Logger(npmCredentials.REALM) + log := logging.Context().Logger(npmLogin.REALM) log.Debug("reading package.json from tarball") var pkg *Package pkg, err = prepare(data) @@ -64,33 +64,10 @@ func (b *artifactHandler) StoreBlob(blob cpi.BlobAccess, _ string, _ string, _ c log = log.WithValues("package", pkg.Name, "version", pkg.Version) log.Debug("identified") - // get credentials and TODO cache it - cred := npmCredentials.GetCredentials(ctx.GetContext(), b.spec.Url, pkg.Name) - if cred == nil { - return nil, fmt.Errorf("No credentials found for %s. Couldn't upload '%s'.", b.spec.Url, pkg.Name) - } - log.Debug("found credentials") - - // check if token exists, if not login and retrieve token - token := cred[npmCredentials.ATTR_TOKEN] - if token == "" { - // use user+pass+mail from credentials to login and retrieve bearer token - username := cred[npmCredentials.ATTR_USERNAME] - password := cred[npmCredentials.ATTR_PASSWORD] - email := cred[npmCredentials.ATTR_EMAIL] - if username == "" || password == "" || email == "" { - return nil, fmt.Errorf("No credentials for %s are invalid. Username, password or email missing! Couldn't upload '%s'.", b.spec.Url, pkg.Name) - } - log = log.WithValues("user", username, "repo", b.spec.Url) - log.Debug("login") - - // TODO: check different kinds of .npmrc content - token, err = login(b.spec.Url, username, password, email) - if err != nil { - return nil, err - } - } else { - log.Debug("token found, skipping login") + 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 diff --git a/pkg/contexts/ocm/blobhandler/handlers/generic/npm/publish.go b/pkg/contexts/ocm/blobhandler/handlers/generic/npm/publish.go index cb5058e662..d27a306a1d 100644 --- a/pkg/contexts/ocm/blobhandler/handlers/generic/npm/publish.go +++ b/pkg/contexts/ocm/blobhandler/handlers/generic/npm/publish.go @@ -6,7 +6,6 @@ import ( "archive/tar" "bytes" "compress/gzip" - "context" //nolint:gosec // older npm (prior to v5) uses sha1 "crypto/sha1" "crypto/sha512" @@ -16,8 +15,6 @@ import ( "errors" "fmt" "io" - "net/http" - "net/url" ) type Package struct { @@ -58,44 +55,6 @@ func NewAttachment(data []byte) *Attachment { } } -// Login to npm registry (URL) and retrieve bearer token. -func login(registry string, username string, password string, email string) (string, error) { - data := map[string]interface{}{ - "_id": "org.couchdb.user:" + username, - "name": username, - "email": email, - "password": password, - "type": "user", - } - marshal, err := json.Marshal(data) - if err != nil { - return "", err - } - req, err := http.NewRequestWithContext(context.Background(), http.MethodPut, registry+"/-/user/org.couchdb.user:"+url.PathEscape(username), bytes.NewReader(marshal)) - if err != nil { - return "", err - } - req.SetBasicAuth(username, password) - req.Header.Set("content-type", "application/json") - resp, err := http.DefaultClient.Do(req) - if err != nil { - return "", err - } - defer resp.Body.Close() - if resp.StatusCode >= http.StatusBadRequest { - all, _ := io.ReadAll(resp.Body) - return "", fmt.Errorf("%d, %s", resp.StatusCode, string(all)) - } - var token struct { - Token string `json:"token"` - } - err = json.NewDecoder(resp.Body).Decode(&token) - if err != nil { - return "", err - } - return token.Token, nil -} - func createSha512(data []byte) string { hash := sha512.New() hash.Write(data) diff --git a/pkg/npm/login.go b/pkg/npm/login.go new file mode 100644 index 0000000000..17f45b0298 --- /dev/null +++ b/pkg/npm/login.go @@ -0,0 +1,99 @@ +package npm + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + + "github.com/open-component-model/ocm/pkg/contexts/credentials/builtin/npm/identity" + "github.com/open-component-model/ocm/pkg/contexts/credentials/cpi" + "github.com/open-component-model/ocm/pkg/logging" +) + +var REALM = identity.REALM + +// Login to npm registry (URL) and retrieve bearer token. +func Login(registry string, username string, password string, email string) (string, error) { + data := map[string]interface{}{ + "_id": "org.couchdb.user:" + username, + "name": username, + "email": email, + "password": password, + "type": "user", + } + marshal, err := json.Marshal(data) + if err != nil { + return "", err + } + req, err := http.NewRequestWithContext(context.Background(), http.MethodPut, registry+"/-/user/org.couchdb.user:"+url.PathEscape(username), bytes.NewReader(marshal)) + if err != nil { + return "", err + } + req.SetBasicAuth(username, password) + req.Header.Set("content-type", "application/json") + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + if resp.StatusCode >= http.StatusBadRequest { + all, _ := io.ReadAll(resp.Body) + return "", fmt.Errorf("%d, %s", resp.StatusCode, string(all)) + } + var token struct { + Token string `json:"token"` + } + err = json.NewDecoder(resp.Body).Decode(&token) + if err != nil { + return "", err + } + return token.Token, nil +} + +// BearerToken retrieves the bearer token for the given repository URL and package name. +// Either it's setup in the credentials or it will login to the registry and retrieve it. +func BearerToken(ctx cpi.ContextProvider, repoUrl string, pkgName string) (string, error) { + // get credentials and TODO cache it + cred := identity.GetCredentials(ctx, repoUrl, pkgName) + if cred == nil { + return "", fmt.Errorf("no credentials found for %s. Couldn't upload '%s'", repoUrl, pkgName) + } + log := logging.Context().Logger(identity.REALM) + log.Debug("found credentials") + + // check if token exists, if not login and retrieve token + token := cred[identity.ATTR_TOKEN] + if token != "" { + log.Debug("token found, skipping login") + return token, nil + } + + // use user+pass+mail from credentials to login and retrieve bearer token + username := cred[identity.ATTR_USERNAME] + password := cred[identity.ATTR_PASSWORD] + email := cred[identity.ATTR_EMAIL] + if username == "" || password == "" || email == "" { + return "", fmt.Errorf("credentials for %s are invalid. Username, password or email missing! Couldn't upload '%s'", repoUrl, pkgName) + } + log = log.WithValues("user", username, "repo", repoUrl) + log.Debug("login") + + // TODO: check different kinds of .npmrc content + return Login(repoUrl, username, password, email) +} + +// Authorize the given request with the bearer token for the given repository URL and package name. +// If the token is empty (login failed or credentials not found), it will not be set. +func Authorize(req *http.Request, ctx cpi.ContextProvider, repoUrl string, pkgName string) { + token, err := BearerToken(ctx, repoUrl, pkgName) + if err != nil { + log := logging.Context().Logger(identity.REALM) + log.Debug("Couldn't authorize", "error", err.Error(), "repo", repoUrl, "package", pkgName) + } else if token != "" { + req.Header.Set("authorization", "Bearer "+token) + } +}