Skip to content

Commit

Permalink
Npm/auth access (#757)
Browse files Browse the repository at this point in the history
## 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
  • Loading branch information
hilmarf authored May 6, 2024
1 parent 975a485 commit 4473dac
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 82 deletions.
30 changes: 18 additions & 12 deletions pkg/contexts/ocm/accessmethods/npm/method.go
Original file line number Diff line number Diff line change
@@ -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 (
Expand All @@ -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"
)

Expand Down Expand Up @@ -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
}
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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
Expand Down
35 changes: 6 additions & 29 deletions pkg/contexts/ocm/blobhandler/handlers/generic/npm/blobhandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down
41 changes: 0 additions & 41 deletions pkg/contexts/ocm/blobhandler/handlers/generic/npm/publish.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"archive/tar"
"bytes"
"compress/gzip"
"context"
//nolint:gosec // older npm (prior to v5) uses sha1
"crypto/sha1"
"crypto/sha512"
Expand All @@ -16,8 +15,6 @@ import (
"errors"
"fmt"
"io"
"net/http"
"net/url"
)

type Package struct {
Expand Down Expand Up @@ -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)
Expand Down
99 changes: 99 additions & 0 deletions pkg/npm/login.go
Original file line number Diff line number Diff line change
@@ -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)
}
}

0 comments on commit 4473dac

Please sign in to comment.