feat(repodb): add user related information to repodb (#1317)

Initial code was contributed by Bogdan BIVOLARU <104334+bogdanbiv@users.noreply.github.com>
Moved implementation from a separate db to repodb by Andrei Aaron <aaaron@luxoft.com>

Not done yet:
- run/test dynamodb implementation, only boltdb was tested
- add additional coverage for existing functionality
- add web-based APIs to toggle the stars/bookmarks on/off

Initially graphql mutation was discussed for the missing API but
we decided REST endpoints would be better suited for configuration



feat(userdb): complete functionality for userdb integration

- dynamodb rollback changes to user starred repos in case increasing the total star count fails
- dynamodb increment/decrement repostars in repometa when user stars/unstars a repo
- dynamodb check anonymous user permissions are working as intendend
- common test handle anonymous users
- RepoMeta2RepoSummary set IsStarred and IsBookmarked



feat(userdb): rest api calls for toggling stars/bookmarks on/off



test(userdb): blackbox tests



test(userdb): move preferences tests in a different file with specific build tags



feat(repodb): add is-starred and is-bookmarked fields to repo-meta

- removed duplicated logic for determining if a repo is starred/bookmarked

Signed-off-by: Laurentiu Niculae <niculae.laurentiu1@gmail.com>
Co-authored-by: Andrei Aaron <aaaron@luxoft.com>
This commit is contained in:
LaurentiuNiculae
2023-04-24 21:13:15 +03:00
committed by GitHub
parent ef51fd692d
commit 9cc990d7ca
50 changed files with 4357 additions and 648 deletions
+143
View File
@@ -0,0 +1,143 @@
//go:build userprefs
// +build userprefs
package extensions
import (
"errors"
"net/http"
"net/url"
"github.com/gorilla/mux"
zerr "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/api/constants"
"zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/meta/repodb"
"zotregistry.io/zot/pkg/storage"
)
const (
ToggleRepoBookmarkAction = "toggleBookmark"
ToggleRepoStarAction = "toggleStar"
)
func SetupUserPreferencesRoutes(config *config.Config, router *mux.Router, storeController storage.StoreController,
repoDB repodb.RepoDB, cveInfo CveInfo, log log.Logger,
) {
if config.Extensions.Search != nil && *config.Extensions.Search.Enable {
log.Info().Msg("setting up user preferences routes")
userprefsRouter := router.PathPrefix(constants.ExtUserPreferencesPrefix).Subrouter()
userprefsRouter.HandleFunc("", HandleUserPrefs(repoDB, log)).Methods(http.MethodPut)
}
}
func HandleUserPrefs(repoDB repodb.RepoDB, log log.Logger) func(w http.ResponseWriter, r *http.Request) {
return func(rsp http.ResponseWriter, req *http.Request) {
if !queryHasParams(req.URL.Query(), []string{"action"}) {
rsp.WriteHeader(http.StatusBadRequest)
return
}
action := req.URL.Query().Get("action")
switch action {
case ToggleRepoBookmarkAction:
PutBookmark(rsp, req, repoDB, log) //nolint:contextcheck
return
case ToggleRepoStarAction:
PutStar(rsp, req, repoDB, log) //nolint:contextcheck
return
default:
rsp.WriteHeader(http.StatusBadRequest)
return
}
}
}
func PutStar(rsp http.ResponseWriter, req *http.Request, repoDB repodb.RepoDB, log log.Logger) {
if !queryHasParams(req.URL.Query(), []string{"repo"}) {
rsp.WriteHeader(http.StatusBadRequest)
return
}
repo := req.URL.Query().Get("repo")
if repo == "" {
rsp.WriteHeader(http.StatusNotFound)
return
}
_, err := repoDB.ToggleStarRepo(req.Context(), repo)
if err != nil {
if errors.Is(err, zerr.ErrRepoMetaNotFound) {
rsp.WriteHeader(http.StatusNotFound)
return
} else if errors.Is(err, zerr.ErrUserDataNotAllowed) {
rsp.WriteHeader(http.StatusForbidden)
return
}
rsp.WriteHeader(http.StatusInternalServerError)
return
}
rsp.WriteHeader(http.StatusOK)
}
func PutBookmark(rsp http.ResponseWriter, req *http.Request, repoDB repodb.RepoDB, log log.Logger) {
if !queryHasParams(req.URL.Query(), []string{"repo"}) {
rsp.WriteHeader(http.StatusBadRequest)
return
}
repo := req.URL.Query().Get("repo")
if repo == "" {
rsp.WriteHeader(http.StatusNotFound)
return
}
_, err := repoDB.ToggleBookmarkRepo(req.Context(), repo)
if err != nil {
if errors.Is(err, zerr.ErrRepoMetaNotFound) {
rsp.WriteHeader(http.StatusNotFound)
return
} else if errors.Is(err, zerr.ErrUserDataNotAllowed) {
rsp.WriteHeader(http.StatusForbidden)
return
}
rsp.WriteHeader(http.StatusInternalServerError)
return
}
rsp.WriteHeader(http.StatusOK)
}
func queryHasParams(values url.Values, params []string) bool {
for _, param := range params {
if !values.Has(param) {
return false
}
}
return true
}
@@ -0,0 +1,20 @@
//go:build !userprefs
// +build !userprefs
package extensions
import (
"github.com/gorilla/mux"
"zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/meta/repodb"
"zotregistry.io/zot/pkg/storage"
)
func SetupUserPreferencesRoutes(config *config.Config, router *mux.Router, storeController storage.StoreController,
repoDB repodb.RepoDB, cveInfo CveInfo, log log.Logger,
) {
log.Warn().Msg("userprefs extension is disabled because given zot binary doesn't" +
"include this feature please build a binary that does so")
}
+139
View File
@@ -0,0 +1,139 @@
//go:build userprefs
// +build userprefs
package extensions_test
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/gorilla/mux"
. "github.com/smartystreets/goconvey/convey"
zerr "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/extensions"
"zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/meta/repodb"
"zotregistry.io/zot/pkg/test/mocks"
)
var ErrTestError = errors.New("TestError")
const UserprefsBaseURL = "http://127.0.0.1:8080/v2/_zot/ext/userprefs"
func TestHandlers(t *testing.T) {
log := log.NewLogger("debug", "")
mockrepoDB := mocks.RepoDBMock{}
Convey("No repo in request", t, func() {
request := httptest.NewRequest("GET", UserprefsBaseURL+"", strings.NewReader("My string"))
response := httptest.NewRecorder()
extensions.PutStar(response, request, mockrepoDB, log)
res := response.Result()
So(res.StatusCode, ShouldEqual, http.StatusBadRequest)
defer res.Body.Close()
extensions.PutBookmark(response, request, mockrepoDB, log)
res = response.Result()
So(res.StatusCode, ShouldEqual, http.StatusBadRequest)
defer res.Body.Close()
})
Convey("Empty repo in request", t, func() {
request := httptest.NewRequest("GET", UserprefsBaseURL+"?repo=", strings.NewReader("My string"))
response := httptest.NewRecorder()
extensions.PutStar(response, request, mockrepoDB, log)
res := response.Result()
So(res.StatusCode, ShouldEqual, http.StatusNotFound)
defer res.Body.Close()
extensions.PutBookmark(response, request, mockrepoDB, log)
res = response.Result()
So(res.StatusCode, ShouldEqual, http.StatusNotFound)
defer res.Body.Close()
})
Convey("ToggleStarRepo different errors", t, func() {
request := httptest.NewRequest("GET", UserprefsBaseURL+"?repo=test",
strings.NewReader("My string"))
Convey("ErrRepoMetaNotFound", func() {
mockrepoDB.ToggleStarRepoFn = func(ctx context.Context, repo string) (repodb.ToggleState, error) {
return repodb.NotChanged, zerr.ErrRepoMetaNotFound
}
mockrepoDB.ToggleBookmarkRepoFn = func(ctx context.Context, repo string) (repodb.ToggleState, error) {
return repodb.NotChanged, zerr.ErrRepoMetaNotFound
}
response := httptest.NewRecorder()
extensions.PutBookmark(response, request, mockrepoDB, log)
res := response.Result()
So(res.StatusCode, ShouldEqual, http.StatusNotFound)
defer res.Body.Close()
response = httptest.NewRecorder()
extensions.PutStar(response, request, mockrepoDB, log)
res = response.Result()
So(res.StatusCode, ShouldEqual, http.StatusNotFound)
defer res.Body.Close()
})
Convey("ErrUserDataNotAllowed", func() {
request = mux.SetURLVars(request, map[string]string{
"name": "repo",
})
mockrepoDB.ToggleBookmarkRepoFn = func(ctx context.Context, repo string) (repodb.ToggleState, error) {
return repodb.NotChanged, zerr.ErrUserDataNotAllowed
}
mockrepoDB.ToggleStarRepoFn = func(ctx context.Context, repo string) (repodb.ToggleState, error) {
return repodb.NotChanged, zerr.ErrUserDataNotAllowed
}
response := httptest.NewRecorder()
extensions.PutBookmark(response, request, mockrepoDB, log)
res := response.Result()
So(res.StatusCode, ShouldEqual, http.StatusForbidden)
defer res.Body.Close()
response = httptest.NewRecorder()
extensions.PutStar(response, request, mockrepoDB, log)
res = response.Result()
So(res.StatusCode, ShouldEqual, http.StatusForbidden)
defer res.Body.Close()
})
Convey("ErrUnexpectedError", func() {
request = mux.SetURLVars(request, map[string]string{
"name": "repo",
})
mockrepoDB.ToggleBookmarkRepoFn = func(ctx context.Context, repo string) (repodb.ToggleState, error) {
return repodb.NotChanged, ErrTestError
}
mockrepoDB.ToggleStarRepoFn = func(ctx context.Context, repo string) (repodb.ToggleState, error) {
return repodb.NotChanged, ErrTestError
}
response := httptest.NewRecorder()
extensions.PutBookmark(response, request, mockrepoDB, log)
res := response.Result()
So(res.StatusCode, ShouldEqual, http.StatusInternalServerError)
defer res.Body.Close()
response = httptest.NewRecorder()
extensions.PutStar(response, request, mockrepoDB, log)
res = response.Result()
So(res.StatusCode, ShouldEqual, http.StatusInternalServerError)
defer res.Body.Close()
})
})
}
+12 -10
View File
@@ -31,15 +31,15 @@ func RepoMeta2RepoSummary(ctx context.Context, repoMeta repodb.RepoMetadata,
skip SkipQGLField, cveInfo cveinfo.CveInfo,
) *gql_generated.RepoSummary {
var (
repoName = repoMeta.Name
repoLastUpdatedTimestamp = time.Time{}
repoPlatformsSet = map[string]*gql_generated.Platform{}
repoVendorsSet = map[string]bool{}
lastUpdatedImageSummary *gql_generated.ImageSummary
repoStarCount = repoMeta.Stars
isBookmarked = false
isStarred = false
repoDownloadCount = 0
repoName = repoMeta.Name
repoStarCount = repoMeta.Stars // total number of stars
repoIsUserStarred = repoMeta.IsStarred // value specific to the current user
repoIsUserBookMarked = repoMeta.IsBookmarked // value specific to the current user
// map used to keep track of all blobs of a repo without dublicates as
// some images may have the same layers
@@ -88,6 +88,7 @@ func RepoMeta2RepoSummary(ctx context.Context, repoMeta repodb.RepoMetadata,
repoSize := strconv.FormatInt(size, 10)
repoPlatforms := make([]*gql_generated.Platform, 0, len(repoPlatformsSet))
for _, platform := range repoPlatformsSet {
repoPlatforms = append(repoPlatforms, platform)
}
@@ -129,8 +130,8 @@ func RepoMeta2RepoSummary(ctx context.Context, repoMeta repodb.RepoMetadata,
NewestImage: lastUpdatedImageSummary,
DownloadCount: &repoDownloadCount,
StarCount: &repoStarCount,
IsBookmarked: &isBookmarked,
IsStarred: &isStarred,
IsBookmarked: &repoIsUserBookMarked,
IsStarred: &repoIsUserStarred,
}
}
@@ -574,15 +575,15 @@ func RepoMeta2ExpandedRepoInfo(ctx context.Context, repoMeta repodb.RepoMetadata
skip SkipQGLField, cveInfo cveinfo.CveInfo, log log.Logger,
) (*gql_generated.RepoSummary, []*gql_generated.ImageSummary) {
var (
repoName = repoMeta.Name
repoLastUpdatedTimestamp = time.Time{}
repoPlatformsSet = map[string]*gql_generated.Platform{}
repoVendorsSet = map[string]bool{}
lastUpdatedImageSummary *gql_generated.ImageSummary
repoStarCount = repoMeta.Stars
isBookmarked = false
isStarred = false
repoDownloadCount = 0
repoName = repoMeta.Name
repoStarCount = repoMeta.Stars // total number of stars
isStarred = repoMeta.IsStarred // value specific to the current user
isBookmarked = repoMeta.IsBookmarked // value specific to the current user
// map used to keep track of all blobs of a repo without dublicates as
// some images may have the same layers
@@ -632,6 +633,7 @@ func RepoMeta2ExpandedRepoInfo(ctx context.Context, repoMeta repodb.RepoMetadata
repoSize := strconv.FormatInt(size, 10)
repoPlatforms := make([]*gql_generated.Platform, 0, len(repoPlatformsSet))
for _, platform := range repoPlatformsSet {
repoPlatforms = append(repoPlatforms, platform)
}
+3 -2
View File
@@ -86,7 +86,8 @@ func TestDigestSearchHTTP(t *testing.T) {
)
So(err, ShouldBeNil)
image1.Reference = "0.0.1"
const ver001 = "0.0.1"
image1.Reference = ver001
err = UploadImage(
image1,
baseURL,
@@ -109,7 +110,7 @@ func TestDigestSearchHTTP(t *testing.T) {
)
So(err, ShouldBeNil)
image2.Reference = "0.0.1"
image2.Reference = ver001
manifestDigest, err := image2.Digest()
So(err, ShouldBeNil)
@@ -156,6 +156,7 @@ type ComplexityRoot struct {
Query struct {
BaseImageList func(childComplexity int, image string, digest *string, requestedPage *PageInput) int
BookmarkedRepos func(childComplexity int, requestedPage *PageInput) int
CVEListForImage func(childComplexity int, image string, requestedPage *PageInput, searchedCve *string) int
DerivedImageList func(childComplexity int, image string, digest *string, requestedPage *PageInput) int
ExpandedRepoInfo func(childComplexity int, repo string) int
@@ -167,6 +168,7 @@ type ComplexityRoot struct {
ImageListWithCVEFixed func(childComplexity int, id string, image string, requestedPage *PageInput) int
Referrers func(childComplexity int, repo string, digest string, typeArg []string) int
RepoListWithNewestImage func(childComplexity int, requestedPage *PageInput) int
StarredRepos func(childComplexity int, requestedPage *PageInput) int
}
Referrer struct {
@@ -209,6 +211,8 @@ type QueryResolver interface {
BaseImageList(ctx context.Context, image string, digest *string, requestedPage *PageInput) (*PaginatedImagesResult, error)
Image(ctx context.Context, image string) (*ImageSummary, error)
Referrers(ctx context.Context, repo string, digest string, typeArg []string) ([]*Referrer, error)
StarredRepos(ctx context.Context, requestedPage *PageInput) (*PaginatedReposResult, error)
BookmarkedRepos(ctx context.Context, requestedPage *PageInput) (*PaginatedReposResult, error)
}
type executableSchema struct {
@@ -700,6 +704,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Query.BaseImageList(childComplexity, args["image"].(string), args["digest"].(*string), args["requestedPage"].(*PageInput)), true
case "Query.BookmarkedRepos":
if e.complexity.Query.BookmarkedRepos == nil {
break
}
args, err := ec.field_Query_BookmarkedRepos_args(context.TODO(), rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Query.BookmarkedRepos(childComplexity, args["requestedPage"].(*PageInput)), true
case "Query.CVEListForImage":
if e.complexity.Query.CVEListForImage == nil {
break
@@ -832,6 +848,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Query.RepoListWithNewestImage(childComplexity, args["requestedPage"].(*PageInput)), true
case "Query.StarredRepos":
if e.complexity.Query.StarredRepos == nil {
break
}
args, err := ec.field_Query_StarredRepos_args(context.TODO(), rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Query.StarredRepos(childComplexity, args["requestedPage"].(*PageInput)), true
case "Referrer.Annotations":
if e.complexity.Referrer.Annotations == nil {
break
@@ -1688,6 +1716,22 @@ type Query {
"Types of artifacts to return in the referrer list"
type: [String!]
): [Referrer]!
"""
Receive RepoSummaries of repos starred by current user
"""
StarredRepos(
"Sets the parameters of the requested page (how many to include and offset)"
requestedPage: PageInput
): PaginatedReposResult!
"""
Receive RepoSummaries of repos bookmarked by current user
"""
BookmarkedRepos(
"Sets the parameters of the requested page (how many to include and offset)"
requestedPage: PageInput
): PaginatedReposResult!
}
`, BuiltIn: false},
}
@@ -1730,6 +1774,21 @@ func (ec *executionContext) field_Query_BaseImageList_args(ctx context.Context,
return args, nil
}
func (ec *executionContext) field_Query_BookmarkedRepos_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
var arg0 *PageInput
if tmp, ok := rawArgs["requestedPage"]; ok {
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("requestedPage"))
arg0, err = ec.unmarshalOPageInput2ᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐPageInput(ctx, tmp)
if err != nil {
return nil, err
}
}
args["requestedPage"] = arg0
return args, nil
}
func (ec *executionContext) field_Query_CVEListForImage_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
@@ -2012,6 +2071,21 @@ func (ec *executionContext) field_Query_RepoListWithNewestImage_args(ctx context
return args, nil
}
func (ec *executionContext) field_Query_StarredRepos_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
var arg0 *PageInput
if tmp, ok := rawArgs["requestedPage"]; ok {
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("requestedPage"))
arg0, err = ec.unmarshalOPageInput2ᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐPageInput(ctx, tmp)
if err != nil {
return nil, err
}
}
args["requestedPage"] = arg0
return args, nil
}
func (ec *executionContext) field_Query___type_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
@@ -5831,6 +5905,128 @@ func (ec *executionContext) fieldContext_Query_Referrers(ctx context.Context, fi
return fc, nil
}
func (ec *executionContext) _Query_StarredRepos(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Query_StarredRepos(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Query().StarredRepos(rctx, fc.Args["requestedPage"].(*PageInput))
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(*PaginatedReposResult)
fc.Result = res
return ec.marshalNPaginatedReposResult2ᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐPaginatedReposResult(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_Query_StarredRepos(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Query",
Field: field,
IsMethod: true,
IsResolver: true,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
switch field.Name {
case "Page":
return ec.fieldContext_PaginatedReposResult_Page(ctx, field)
case "Results":
return ec.fieldContext_PaginatedReposResult_Results(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type PaginatedReposResult", field.Name)
},
}
defer func() {
if r := recover(); r != nil {
err = ec.Recover(ctx, r)
ec.Error(ctx, err)
}
}()
ctx = graphql.WithFieldContext(ctx, fc)
if fc.Args, err = ec.field_Query_StarredRepos_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
ec.Error(ctx, err)
return
}
return fc, nil
}
func (ec *executionContext) _Query_BookmarkedRepos(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Query_BookmarkedRepos(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Query().BookmarkedRepos(rctx, fc.Args["requestedPage"].(*PageInput))
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(*PaginatedReposResult)
fc.Result = res
return ec.marshalNPaginatedReposResult2ᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐPaginatedReposResult(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_Query_BookmarkedRepos(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Query",
Field: field,
IsMethod: true,
IsResolver: true,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
switch field.Name {
case "Page":
return ec.fieldContext_PaginatedReposResult_Page(ctx, field)
case "Results":
return ec.fieldContext_PaginatedReposResult_Results(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type PaginatedReposResult", field.Name)
},
}
defer func() {
if r := recover(); r != nil {
err = ec.Recover(ctx, r)
ec.Error(ctx, err)
}
}()
ctx = graphql.WithFieldContext(ctx, fc)
if fc.Args, err = ec.field_Query_BookmarkedRepos_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
ec.Error(ctx, err)
return
}
return fc, nil
}
func (ec *executionContext) _Query___type(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Query___type(ctx, field)
if err != nil {
@@ -9526,6 +9722,52 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr
return ec.OperationContext.RootResolverMiddleware(ctx, innerFunc)
}
out.Concurrently(i, func() graphql.Marshaler {
return rrm(innerCtx)
})
case "StarredRepos":
field := field
innerFunc := func(ctx context.Context) (res graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
}
}()
res = ec._Query_StarredRepos(ctx, field)
if res == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
return res
}
rrm := func(ctx context.Context) graphql.Marshaler {
return ec.OperationContext.RootResolverMiddleware(ctx, innerFunc)
}
out.Concurrently(i, func() graphql.Marshaler {
return rrm(innerCtx)
})
case "BookmarkedRepos":
field := field
innerFunc := func(ctx context.Context) (res graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
}
}()
res = ec._Query_BookmarkedRepos(ctx, field)
if res == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
return res
}
rrm := func(ctx context.Context) graphql.Marshaler {
return ec.OperationContext.RootResolverMiddleware(ctx, innerFunc)
}
out.Concurrently(i, func() graphql.Marshaler {
return rrm(innerCtx)
})
+89 -4
View File
@@ -18,7 +18,7 @@ import (
"github.com/vektah/gqlparser/v2/gqlerror"
zerr "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/common"
zcommon "zotregistry.io/zot/pkg/common"
"zotregistry.io/zot/pkg/extensions/search/convert"
cveinfo "zotregistry.io/zot/pkg/extensions/search/cve"
cvemodel "zotregistry.io/zot/pkg/extensions/search/cve/model"
@@ -300,7 +300,7 @@ func getCVEListForImage(
),
}
repo, ref, isTag := common.GetImageDirAndReference(image)
repo, ref, isTag := zcommon.GetImageDirAndReference(image)
if ref == "" {
return &gql_generated.CVEResultForImage{}, gqlerror.Errorf("no reference provided")
@@ -560,6 +560,91 @@ func repoListWithNewestImage(
return paginatedRepos, nil
}
func getBookmarkedRepos(
ctx context.Context,
cveInfo cveinfo.CveInfo,
log log.Logger, //nolint:unparam // may be used by devs for debugging
requestedPage *gql_generated.PageInput,
repoDB repodb.RepoDB,
) (*gql_generated.PaginatedReposResult, error) {
repoNames, err := repoDB.GetBookmarkedRepos(ctx)
if err != nil {
return &gql_generated.PaginatedReposResult{}, err
}
filterFn := func(repoMeta repodb.RepoMetadata) bool {
return zcommon.Contains(repoNames, repoMeta.Name)
}
return getFilteredPaginatedRepos(ctx, cveInfo, filterFn, log, requestedPage, repoDB)
}
func getStarredRepos(
ctx context.Context,
cveInfo cveinfo.CveInfo,
log log.Logger, //nolint:unparam // may be used by devs for debugging
requestedPage *gql_generated.PageInput,
repoDB repodb.RepoDB,
) (*gql_generated.PaginatedReposResult, error) {
repoNames, err := repoDB.GetStarredRepos(ctx)
if err != nil {
return &gql_generated.PaginatedReposResult{}, err
}
filterFn := func(repoMeta repodb.RepoMetadata) bool {
return zcommon.Contains(repoNames, repoMeta.Name)
}
return getFilteredPaginatedRepos(ctx, cveInfo, filterFn, log, requestedPage, repoDB)
}
func getFilteredPaginatedRepos(
ctx context.Context,
cveInfo cveinfo.CveInfo,
filterFn repodb.FilterRepoFunc,
log log.Logger, //nolint:unparam // may be used by devs for debugging
requestedPage *gql_generated.PageInput,
repoDB repodb.RepoDB,
) (*gql_generated.PaginatedReposResult, error) {
repos := []*gql_generated.RepoSummary{}
paginatedRepos := &gql_generated.PaginatedReposResult{}
if requestedPage == nil {
requestedPage = &gql_generated.PageInput{}
}
skip := convert.SkipQGLField{
Vulnerabilities: canSkipField(convert.GetPreloads(ctx), "Results.NewestImage.Vulnerabilities"),
}
pageInput := repodb.PageInput{
Limit: safeDerefferencing(requestedPage.Limit, 0),
Offset: safeDerefferencing(requestedPage.Offset, 0),
SortBy: repodb.SortCriteria(
safeDerefferencing(requestedPage.SortBy, gql_generated.SortCriteriaUpdateTime),
),
}
reposMeta, manifestMetaMap, indexDataMap, pageInfo, err := repoDB.FilterRepos(ctx, filterFn, pageInput)
if err != nil {
return paginatedRepos, err
}
for _, repoMeta := range reposMeta {
repoSummary := convert.RepoMeta2RepoSummary(ctx, repoMeta, manifestMetaMap, indexDataMap,
skip, cveInfo)
repos = append(repos, repoSummary)
}
paginatedRepos.Page = &gql_generated.PageInfo{
TotalCount: pageInfo.TotalCount,
ItemCount: pageInfo.ItemCount,
}
paginatedRepos.Results = repos
return paginatedRepos, nil
}
func globalSearch(ctx context.Context, query string, repoDB repodb.RepoDB, filter *gql_generated.Filter,
requestedPage *gql_generated.PageInput, cveInfo cveinfo.CveInfo, log log.Logger, //nolint:unparam
) (*gql_generated.PaginatedReposResult, []*gql_generated.ImageSummary, []*gql_generated.LayerSummary, error,
@@ -675,7 +760,7 @@ func derivedImageList(ctx context.Context, image string, digest *string, repoDB
Vulnerabilities: canSkipField(convert.GetPreloads(ctx), "Vulnerabilities"),
}
imageRepo, imageTag := common.GetImageDirAndTag(image)
imageRepo, imageTag := zcommon.GetImageDirAndTag(image)
if imageTag == "" {
return &gql_generated.PaginatedImagesResult{}, gqlerror.Errorf("no reference provided")
}
@@ -788,7 +873,7 @@ func baseImageList(ctx context.Context, image string, digest *string, repoDB rep
Vulnerabilities: canSkipField(convert.GetPreloads(ctx), "Vulnerabilities"),
}
imageRepo, imageTag := common.GetImageDirAndTag(image)
imageRepo, imageTag := zcommon.GetImageDirAndTag(image)
if imageTag == "" {
return &gql_generated.PaginatedImagesResult{}, gqlerror.Errorf("no reference provided")
+164 -8
View File
@@ -22,6 +22,7 @@ import (
"zotregistry.io/zot/pkg/meta/bolt"
"zotregistry.io/zot/pkg/meta/repodb"
boltdb_wrapper "zotregistry.io/zot/pkg/meta/repodb/boltdb-wrapper"
localCtx "zotregistry.io/zot/pkg/requestcontext"
"zotregistry.io/zot/pkg/storage"
"zotregistry.io/zot/pkg/test/mocks"
)
@@ -418,7 +419,7 @@ func TestRepoListWithNewestImage(t *testing.T) {
So(repos.Results, ShouldBeEmpty)
})
Convey("RepoDB SearchRepo Bad manifest referenced", func() {
Convey("RepoDB SearchRepo bad manifest referenced", func() {
mockRepoDB := mocks.RepoDBMock{
SearchReposFn: func(ctx context.Context, searchText string, filter repodb.Filter, requestedPage repodb.PageInput,
) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, map[string]repodb.IndexData, repodb.PageInfo, error) {
@@ -546,8 +547,8 @@ func TestRepoListWithNewestImage(t *testing.T) {
for _, repoMeta := range repos {
pageFinder.Add(repodb.DetailedRepoMeta{
RepoMeta: repoMeta,
UpdateTime: createTime,
RepoMetadata: repoMeta,
UpdateTime: createTime,
})
createTime = createTime.Add(time.Second)
}
@@ -622,6 +623,68 @@ func TestRepoListWithNewestImage(t *testing.T) {
})
}
func TestGetBookmarkedRepos(t *testing.T) {
Convey("getBookmarkedRepos", t, func() {
responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter,
graphql.DefaultRecover)
_, err := getBookmarkedRepos(
responseContext,
mocks.CveInfoMock{},
log.NewLogger("debug", ""),
nil,
mocks.RepoDBMock{
GetBookmarkedReposFn: func(ctx context.Context) ([]string, error) {
return []string{}, ErrTestError
},
},
)
So(err, ShouldNotBeNil)
})
}
func TestGetStarredRepos(t *testing.T) {
Convey("getStarredRepos", t, func() {
responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter,
graphql.DefaultRecover)
_, err := getStarredRepos(
responseContext,
mocks.CveInfoMock{},
log.NewLogger("debug", ""),
nil,
mocks.RepoDBMock{
GetStarredReposFn: func(ctx context.Context) ([]string, error) {
return []string{}, ErrTestError
},
},
)
So(err, ShouldNotBeNil)
})
}
func TestGetFilteredPaginatedRepos(t *testing.T) {
Convey("getFilteredPaginatedRepos FilterRepos fails", t, func() {
responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter,
graphql.DefaultRecover)
_, err := getFilteredPaginatedRepos(
responseContext,
mocks.CveInfoMock{},
func(repoMeta repodb.RepoMetadata) bool { return true },
log.NewLogger("debug", ""),
nil,
mocks.RepoDBMock{
FilterReposFn: func(ctx context.Context, filter repodb.FilterRepoFunc, requestedPage repodb.PageInput,
) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, map[string]repodb.IndexData, repodb.PageInfo,
error,
) {
return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, map[string]repodb.IndexData{},
repodb.PageInfo{}, ErrTestError
},
},
)
So(err, ShouldNotBeNil)
})
}
func TestImageListForDigest(t *testing.T) {
Convey("getImageList", t, func() {
Convey("no page requested, FilterTagsFn returns error", func() {
@@ -1028,7 +1091,7 @@ func TestImageListForDigest(t *testing.T) {
repos[i].Tags = matchedTags
pageFinder.Add(repodb.DetailedRepoMeta{
RepoMeta: repo,
RepoMetadata: repo,
})
}
@@ -2486,6 +2549,28 @@ func TestCVEResolvers(t *testing.T) { //nolint:gocyclo
So(len(images.Results), ShouldEqual, 0)
})
})
Convey("Errors for cve resolvers", t, func() {
_, err := getImageListForCVE(
context.Background(),
"id",
mocks.CveInfoMock{
GetImageListForCVEFn: func(repo, cveID string) ([]cvemodel.TagInfo, error) {
return []cvemodel.TagInfo{}, ErrTestError
},
},
nil,
mocks.RepoDBMock{
GetMultipleRepoMetaFn: func(ctx context.Context, filter func(repoMeta repodb.RepoMetadata) bool,
requestedPage repodb.PageInput,
) ([]repodb.RepoMetadata, error) {
return []repodb.RepoMetadata{{}}, nil
},
},
log,
)
So(err, ShouldNotBeNil)
})
}
func getPageInput(limit int, offset int) *gql_generated.PageInput {
@@ -2726,7 +2811,7 @@ func TestDerivedImageList(t *testing.T) {
repos[i].Tags = matchedTags
pageFinder.Add(repodb.DetailedRepoMeta{
RepoMeta: repo,
RepoMetadata: repo,
})
}
repos, pageInfo := pageFinder.Page()
@@ -2989,7 +3074,7 @@ func TestBaseImageList(t *testing.T) {
repos[i].Tags = matchedTags
pageFinder.Add(repodb.DetailedRepoMeta{
RepoMeta: repo,
RepoMetadata: repo,
})
}
@@ -3163,7 +3248,7 @@ func TestBaseImageList(t *testing.T) {
repos[i].Tags = matchedTags
pageFinder.Add(repodb.DetailedRepoMeta{
RepoMeta: repo,
RepoMetadata: repo,
})
}
@@ -3182,6 +3267,8 @@ func TestBaseImageList(t *testing.T) {
}
func TestExpandedRepoInfo(t *testing.T) {
log := log.NewLogger("debug", "")
Convey("ExpandedRepoInfo Errors", t, func() {
responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter,
graphql.DefaultRecover)
@@ -3198,10 +3285,22 @@ func TestExpandedRepoInfo(t *testing.T) {
Digest: "digestIndex",
MediaType: ispec.MediaTypeImageIndex,
},
"tagGetIndexError": {
Digest: "errorIndexDigest",
MediaType: ispec.MediaTypeImageIndex,
},
"tagGoodIndexBadManifests": {
Digest: "goodIndexBadManifests",
MediaType: ispec.MediaTypeImageIndex,
},
"tagGoodIndex1GoodManfest": {
Digest: "goodIndexGoodManfest",
MediaType: ispec.MediaTypeImageIndex,
},
"tagGoodIndex2GoodManfest": {
Digest: "goodIndexGoodManfest",
MediaType: ispec.MediaTypeImageIndex,
},
},
}, nil
},
@@ -3227,6 +3326,16 @@ func TestExpandedRepoInfo(t *testing.T) {
})
So(err, ShouldBeNil)
goodIndexGoodManfestBlob, err := json.Marshal(ispec.Index{
Manifests: []ispec.Descriptor{
{
Digest: "goodManifest",
MediaType: ispec.MediaTypeImageManifest,
},
},
})
So(err, ShouldBeNil)
switch indexDigest {
case "errorIndexDigest":
return repodb.IndexData{}, ErrTestError
@@ -3234,14 +3343,61 @@ func TestExpandedRepoInfo(t *testing.T) {
return repodb.IndexData{
IndexBlob: goodIndexBadManifestsBlob,
}, nil
case "goodIndexGoodManfest":
return repodb.IndexData{
IndexBlob: goodIndexGoodManfestBlob,
}, nil
default:
return repodb.IndexData{}, nil
}
},
}
log := log.NewLogger("debug", "")
_, err := expandedRepoInfo(responseContext, "repo", repoDB, mocks.CveInfoMock{}, log)
So(err, ShouldBeNil)
})
Convey("Access error", t, func() {
authzCtxKey := localCtx.GetContextKey()
acCtxUser := localCtx.AccessControlContext{
ReadGlobPatterns: map[string]bool{
"repo": false,
},
Username: "user",
}
ctx := context.WithValue(context.Background(), authzCtxKey, acCtxUser)
responseContext := graphql.WithResponseContext(ctx, graphql.DefaultErrorPresenter,
graphql.DefaultRecover)
_, err := expandedRepoInfo(responseContext, "repo", mocks.RepoDBMock{}, mocks.CveInfoMock{}, log)
So(err, ShouldBeNil)
})
}
func TestFilterFunctions(t *testing.T) {
Convey("Filter Functions", t, func() {
Convey("FilterByDigest bad manifest blob", func() {
filterFunc := FilterByDigest("digest")
ok := filterFunc(
repodb.RepoMetadata{},
repodb.ManifestMetadata{
ManifestBlob: []byte("bad blob"),
},
)
So(ok, ShouldBeFalse)
})
Convey("filterDerivedImages bad manifest blob", func() {
filterFunc := filterDerivedImages(&gql_generated.ImageSummary{})
ok := filterFunc(
repodb.RepoMetadata{},
repodb.ManifestMetadata{
ManifestBlob: []byte("bad blob"),
},
)
So(ok, ShouldBeFalse)
})
})
}
+16
View File
@@ -680,4 +680,20 @@ type Query {
"Types of artifacts to return in the referrer list"
type: [String!]
): [Referrer]!
"""
Receive RepoSummaries of repos starred by current user
"""
StarredRepos(
"Sets the parameters of the requested page (how many to include and offset)"
requestedPage: PageInput
): PaginatedReposResult!
"""
Receive RepoSummaries of repos bookmarked by current user
"""
BookmarkedRepos(
"Sets the parameters of the requested page (how many to include and offset)"
requestedPage: PageInput
): PaginatedReposResult!
}
+10
View File
@@ -144,6 +144,16 @@ func (r *queryResolver) Referrers(ctx context.Context, repo string, digest strin
return referrers, nil
}
// StarredRepos is the resolver for the StarredRepos field.
func (r *queryResolver) StarredRepos(ctx context.Context, requestedPage *gql_generated.PageInput) (*gql_generated.PaginatedReposResult, error) {
return getStarredRepos(ctx, r.cveInfo, r.log, requestedPage, r.repoDB)
}
// BookmarkedRepos is the resolver for the BookmarkedRepos field.
func (r *queryResolver) BookmarkedRepos(ctx context.Context, requestedPage *gql_generated.PageInput) (*gql_generated.PaginatedReposResult, error) {
return getBookmarkedRepos(ctx, r.cveInfo, r.log, requestedPage, r.repoDB)
}
// Query returns gql_generated.QueryResolver implementation.
func (r *Resolver) Query() gql_generated.QueryResolver { return &queryResolver{r} }
+20
View File
@@ -147,6 +147,26 @@ type ImageSummaryResult struct {
Errors []ErrorGQL `json:"errors"`
}
//nolint:tagliatelle // graphQL schema
type StarredRepos struct {
PaginatedReposResult `json:"StarredRepos"`
}
//nolint:tagliatelle // graphQL schema
type BookmarkedRepos struct {
PaginatedReposResult `json:"BookmarkedRepos"`
}
type StarredReposResponse struct {
StarredRepos `json:"data"`
Errors []ErrorGQL `json:"errors"`
}
type BookmarkedReposResponse struct {
BookmarkedRepos `json:"data"`
Errors []ErrorGQL `json:"errors"`
}
func readFileAndSearchString(filePath string, stringToMatch string, timeout time.Duration) (bool, error) {
ctx, cancelFunc := context.WithTimeout(context.Background(), timeout)
defer cancelFunc()
+632
View File
@@ -0,0 +1,632 @@
//go:build search && userprefs
package search_test
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
"testing"
. "github.com/smartystreets/goconvey/convey"
"golang.org/x/crypto/bcrypt"
"gopkg.in/resty.v1"
"zotregistry.io/zot/pkg/api"
"zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/api/constants"
extconf "zotregistry.io/zot/pkg/extensions/config"
"zotregistry.io/zot/pkg/extensions/monitoring"
"zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/storage"
"zotregistry.io/zot/pkg/storage/local"
. "zotregistry.io/zot/pkg/test"
)
//nolint:dupl
func TestUserData(t *testing.T) {
Convey("Test user stars and bookmarks", t, func(c C) {
port := GetFreePort()
baseURL := GetBaseURL(port)
defaultVal := true
accessibleRepo := "accessible-repo"
forbiddenRepo := "forbidden-repo"
tag := "0.0.1"
adminUser := "alice"
adminPassword := "deepGoesTheRabbitBurrow"
simpleUser := "test"
simpleUserPassword := "test123"
twoCredTests := fmt.Sprintf("%s\n%s\n\n", getCredString(adminUser, adminPassword),
getCredString(simpleUser, simpleUserPassword))
htpasswdPath := MakeHtpasswdFileFromString(twoCredTests)
defer os.Remove(htpasswdPath)
conf := config.New()
conf.Storage.RootDirectory = t.TempDir()
conf.HTTP.Port = port
conf.HTTP.Auth = &config.AuthConfig{
HTPasswd: config.AuthHTPasswd{
Path: htpasswdPath,
},
}
conf.HTTP.AccessControl = &config.AccessControlConfig{
Repositories: config.Repositories{
"**": config.PolicyGroup{
Policies: []config.Policy{
{
Users: []string{simpleUser},
Actions: []string{"read"},
},
},
AnonymousPolicy: []string{"read"},
DefaultPolicy: []string{},
},
forbiddenRepo: config.PolicyGroup{
Policies: []config.Policy{
{
Users: []string{},
Actions: []string{},
},
},
DefaultPolicy: []string{},
},
},
AdminPolicy: config.Policy{
Users: []string{adminUser},
Actions: []string{"read", "create", "update"},
},
}
conf.Extensions = &extconf.ExtensionConfig{
Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}},
}
ctlr := api.NewController(conf)
ctlrManager := NewControllerManager(ctlr)
ctlrManager.StartAndWait(port)
defer ctlrManager.StopServer()
config, layers, manifest, err := GetImageComponents(100)
So(err, ShouldBeNil)
err = UploadImageWithBasicAuth(
Image{
Config: config,
Layers: layers,
Manifest: manifest,
Reference: tag,
}, baseURL, accessibleRepo,
adminUser, adminPassword,
)
So(err, ShouldBeNil)
err = UploadImageWithBasicAuth(
Image{
Config: config,
Layers: layers,
Manifest: manifest,
Reference: tag,
}, baseURL, forbiddenRepo,
adminUser, adminPassword,
)
So(err, ShouldBeNil)
userStaredReposQuery := `{
StarredRepos {
Results {
Name StarCount IsStarred
NewestImage { Tag }
}
}
}`
userBookmarkedReposQuery := `{
BookmarkedRepos {
Results {
Name IsBookmarked
NewestImage { Tag }
}
}
}`
userprefsBaseURL := baseURL + constants.FullUserPreferencesPrefix
Convey("Flip starred repo authorized", func(c C) {
clientHTTP := resty.R().SetBasicAuth(simpleUser, simpleUserPassword)
resp, err := clientHTTP.Get(baseURL + constants.FullSearchPrefix +
"?query=" + url.QueryEscape(userStaredReposQuery))
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
responseStruct := StarredReposResponse{}
err = json.Unmarshal(resp.Body(), &responseStruct)
So(err, ShouldBeNil)
So(len(responseStruct.Results), ShouldEqual, 0)
resp, err = clientHTTP.Put(userprefsBaseURL + PutRepoStarURL(accessibleRepo))
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
resp, err = clientHTTP.Get(baseURL + constants.FullSearchPrefix +
"?query=" + url.QueryEscape(userStaredReposQuery))
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
responseStruct = StarredReposResponse{}
err = json.Unmarshal(resp.Body(), &responseStruct)
So(err, ShouldBeNil)
So(len(responseStruct.Results), ShouldEqual, 1)
So(responseStruct.Results[0].Name, ShouldEqual, accessibleRepo)
// need to update RepoSummary according to user settings
So(responseStruct.Results[0].IsStarred, ShouldEqual, true)
So(responseStruct.Results[0].StarCount, ShouldEqual, 1)
resp, err = clientHTTP.Put(userprefsBaseURL + PutRepoStarURL(accessibleRepo))
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
resp, err = clientHTTP.Get(baseURL + constants.FullSearchPrefix +
"?query=" + url.QueryEscape(userStaredReposQuery))
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
responseStruct = StarredReposResponse{}
err = json.Unmarshal(resp.Body(), &responseStruct)
So(err, ShouldBeNil)
So(len(responseStruct.Results), ShouldEqual, 0)
})
Convey("Flip starred repo unauthenticated user", func(c C) {
clientHTTP := resty.R()
resp, err := clientHTTP.Get(baseURL + constants.FullSearchPrefix +
"?query=" + url.QueryEscape(userStaredReposQuery))
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
responseStruct := StarredReposResponse{}
err = json.Unmarshal(resp.Body(), &responseStruct)
So(err, ShouldBeNil)
So(len(responseStruct.Results), ShouldEqual, 0)
resp, err = clientHTTP.Put(userprefsBaseURL + PutRepoStarURL(accessibleRepo))
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusForbidden)
resp, err = clientHTTP.Get(baseURL + constants.FullSearchPrefix +
"?query=" + url.QueryEscape(userStaredReposQuery))
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
responseStruct = StarredReposResponse{}
err = json.Unmarshal(resp.Body(), &responseStruct)
So(err, ShouldBeNil)
So(len(responseStruct.Results), ShouldEqual, 0)
})
Convey("Flip starred repo unauthorized", func(c C) {
clientHTTP := resty.R().SetBasicAuth(simpleUser, simpleUserPassword)
resp, err := clientHTTP.Get(baseURL + constants.FullSearchPrefix +
"?query=" + url.QueryEscape(userStaredReposQuery))
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
responseStruct := StarredReposResponse{}
err = json.Unmarshal(resp.Body(), &responseStruct)
So(err, ShouldBeNil)
So(len(responseStruct.Results), ShouldEqual, 0)
resp, err = clientHTTP.Put(userprefsBaseURL + PutRepoStarURL(forbiddenRepo))
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusForbidden)
resp, err = clientHTTP.Get(baseURL + constants.FullSearchPrefix +
"?query=" + url.QueryEscape(userStaredReposQuery))
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
responseStruct = StarredReposResponse{}
err = json.Unmarshal(resp.Body(), &responseStruct)
So(err, ShouldBeNil)
So(len(responseStruct.Results), ShouldEqual, 0)
})
Convey("Flip starred repo with unauthorized repo and admin user", func(c C) {
clientHTTP := resty.R().SetBasicAuth(adminUser, adminPassword)
resp, err := clientHTTP.Get(baseURL + constants.FullSearchPrefix +
"?query=" + url.QueryEscape(userStaredReposQuery))
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
responseStruct := StarredReposResponse{}
err = json.Unmarshal(resp.Body(), &responseStruct)
So(err, ShouldBeNil)
So(len(responseStruct.Results), ShouldEqual, 0)
resp, err = clientHTTP.Put(userprefsBaseURL + PutRepoStarURL(forbiddenRepo))
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
resp, err = clientHTTP.Get(baseURL + constants.FullSearchPrefix +
"?query=" + url.QueryEscape(userStaredReposQuery))
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
responseStruct = StarredReposResponse{}
err = json.Unmarshal(resp.Body(), &responseStruct)
So(err, ShouldBeNil)
So(len(responseStruct.Results), ShouldEqual, 1)
So(responseStruct.Results[0].Name, ShouldEqual, forbiddenRepo)
// need to update RepoSummary according to user settings
So(responseStruct.Results[0].IsStarred, ShouldEqual, true)
So(responseStruct.Results[0].StarCount, ShouldEqual, 1)
resp, err = clientHTTP.Put(userprefsBaseURL + PutRepoStarURL(forbiddenRepo))
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
resp, err = clientHTTP.Get(baseURL + constants.FullSearchPrefix +
"?query=" + url.QueryEscape(userStaredReposQuery))
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
responseStruct = StarredReposResponse{}
err = json.Unmarshal(resp.Body(), &responseStruct)
So(err, ShouldBeNil)
So(len(responseStruct.Results), ShouldEqual, 0)
})
Convey("Flip bookmark repo authorized", func(c C) {
clientHTTP := resty.R().SetBasicAuth(simpleUser, simpleUserPassword)
resp, err := clientHTTP.Get(baseURL + constants.FullSearchPrefix +
"?query=" + url.QueryEscape(userBookmarkedReposQuery))
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
responseStruct := BookmarkedReposResponse{}
err = json.Unmarshal(resp.Body(), &responseStruct)
So(err, ShouldBeNil)
So(len(responseStruct.Results), ShouldEqual, 0)
resp, err = clientHTTP.Put(userprefsBaseURL + PutRepoBookmarkURL(accessibleRepo))
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
resp, err = clientHTTP.Get(baseURL + constants.FullSearchPrefix +
"?query=" + url.QueryEscape(userBookmarkedReposQuery))
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
responseStruct = BookmarkedReposResponse{}
err = json.Unmarshal(resp.Body(), &responseStruct)
So(err, ShouldBeNil)
So(len(responseStruct.Results), ShouldEqual, 1)
So(responseStruct.Results[0].Name, ShouldEqual, accessibleRepo)
// need to update RepoSummary according to user settings
So(responseStruct.Results[0].IsBookmarked, ShouldEqual, true)
resp, err = clientHTTP.Put(userprefsBaseURL + PutRepoBookmarkURL(accessibleRepo))
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
resp, err = clientHTTP.Get(baseURL + constants.FullSearchPrefix +
"?query=" + url.QueryEscape(userBookmarkedReposQuery))
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
responseStruct = BookmarkedReposResponse{}
err = json.Unmarshal(resp.Body(), &responseStruct)
So(err, ShouldBeNil)
So(len(responseStruct.Results), ShouldEqual, 0)
})
Convey("Flip bookmark repo unauthenticated user", func(c C) {
clientHTTP := resty.R()
resp, err := clientHTTP.Get(baseURL + constants.FullSearchPrefix +
"?query=" + url.QueryEscape(userBookmarkedReposQuery))
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
responseStruct := BookmarkedReposResponse{}
err = json.Unmarshal(resp.Body(), &responseStruct)
So(err, ShouldBeNil)
So(len(responseStruct.Results), ShouldEqual, 0)
resp, err = clientHTTP.Put(userprefsBaseURL + PutRepoBookmarkURL(accessibleRepo))
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusForbidden)
resp, err = clientHTTP.Get(baseURL + constants.FullSearchPrefix +
"?query=" + url.QueryEscape(userBookmarkedReposQuery))
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
responseStruct = BookmarkedReposResponse{}
err = json.Unmarshal(resp.Body(), &responseStruct)
So(err, ShouldBeNil)
So(len(responseStruct.Results), ShouldEqual, 0)
})
Convey("Flip bookmark repo unauthorized", func(c C) {
clientHTTP := resty.R().SetBasicAuth(simpleUser, simpleUserPassword)
resp, err := clientHTTP.Get(baseURL + constants.FullSearchPrefix +
"?query=" + url.QueryEscape(userBookmarkedReposQuery))
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
responseStruct := BookmarkedReposResponse{}
err = json.Unmarshal(resp.Body(), &responseStruct)
So(err, ShouldBeNil)
So(len(responseStruct.Results), ShouldEqual, 0)
resp, err = clientHTTP.Put(userprefsBaseURL + PutRepoBookmarkURL(forbiddenRepo))
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusForbidden)
resp, err = clientHTTP.Get(baseURL + constants.FullSearchPrefix +
"?query=" + url.QueryEscape(userBookmarkedReposQuery))
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
responseStruct = BookmarkedReposResponse{}
err = json.Unmarshal(resp.Body(), &responseStruct)
So(err, ShouldBeNil)
So(len(responseStruct.Results), ShouldEqual, 0)
})
Convey("Flip bookmarked unauthorized repo and admin user", func(c C) {
clientHTTP := resty.R().SetBasicAuth(adminUser, adminPassword)
resp, err := clientHTTP.Get(baseURL + constants.FullSearchPrefix +
"?query=" + url.QueryEscape(userBookmarkedReposQuery))
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
responseStruct := BookmarkedReposResponse{}
err = json.Unmarshal(resp.Body(), &responseStruct)
So(err, ShouldBeNil)
So(len(responseStruct.Results), ShouldEqual, 0)
resp, err = clientHTTP.Put(userprefsBaseURL + PutRepoBookmarkURL(forbiddenRepo))
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
resp, err = clientHTTP.Get(baseURL + constants.FullSearchPrefix +
"?query=" + url.QueryEscape(userBookmarkedReposQuery))
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
responseStruct = BookmarkedReposResponse{}
err = json.Unmarshal(resp.Body(), &responseStruct)
So(err, ShouldBeNil)
So(len(responseStruct.Results), ShouldEqual, 1)
So(responseStruct.Results[0].Name, ShouldEqual, forbiddenRepo)
// need to update RepoSummary according to user settings
So(responseStruct.Results[0].IsBookmarked, ShouldEqual, true)
resp, err = clientHTTP.Put(userprefsBaseURL + PutRepoBookmarkURL(forbiddenRepo))
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
resp, err = clientHTTP.Get(baseURL + constants.FullSearchPrefix +
"?query=" + url.QueryEscape(userBookmarkedReposQuery))
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
responseStruct = BookmarkedReposResponse{}
err = json.Unmarshal(resp.Body(), &responseStruct)
So(err, ShouldBeNil)
So(len(responseStruct.Results), ShouldEqual, 0)
})
})
}
func TestChangingRepoState(t *testing.T) {
port := GetFreePort()
baseURL := GetBaseURL(port)
defaultVal := true
simpleUser := "test"
simpleUserPassword := "test123"
forbiddenRepo := "forbidden"
accesibleRepo := "accesible"
credTests := fmt.Sprintf("%s\n\n", getCredString(simpleUser, simpleUserPassword))
htpasswdPath := MakeHtpasswdFileFromString(credTests)
defer os.Remove(htpasswdPath)
conf := config.New()
conf.Storage.RootDirectory = t.TempDir()
conf.HTTP.Port = port
conf.HTTP.Auth = &config.AuthConfig{
HTPasswd: config.AuthHTPasswd{
Path: htpasswdPath,
},
}
conf.HTTP.AccessControl = &config.AccessControlConfig{
Repositories: config.Repositories{
"**": config.PolicyGroup{
Policies: []config.Policy{
{
Users: []string{simpleUser},
Actions: []string{"read"},
},
},
AnonymousPolicy: []string{"read"},
DefaultPolicy: []string{},
},
forbiddenRepo: config.PolicyGroup{
Policies: []config.Policy{
{
Users: []string{},
Actions: []string{},
},
},
DefaultPolicy: []string{},
},
},
}
conf.Extensions = &extconf.ExtensionConfig{
Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}},
}
gqlStarredRepos := `
{
StarredRepos() {
Results {
Name
StarCount
IsBookmarked
IsStarred
}
}
}
`
gqlBookmarkedRepos := `
{
BookmarkedRepos() {
Results {
Name
StarCount
IsBookmarked
IsStarred
}
}
}
`
ctlr := api.NewController(conf)
img, err := GetRandomImage("tag")
if err != nil {
t.FailNow()
}
// ------ Create the test repos
defaultStore := local.NewImageStore(conf.Storage.RootDirectory, false, 0, false, false,
log.NewLogger("debug", ""), monitoring.NewMetricsServer(false, log.NewLogger("debug", "")), nil, nil)
err = WriteImageToFileSystem(img, accesibleRepo, storage.StoreController{
DefaultStore: defaultStore,
})
if err != nil {
t.FailNow()
}
err = WriteImageToFileSystem(img, forbiddenRepo, storage.StoreController{
DefaultStore: defaultStore,
})
if err != nil {
t.FailNow()
}
ctlrManager := NewControllerManager(ctlr)
ctlrManager.StartAndWait(port)
defer ctlrManager.StopServer()
simpleUserClient := resty.R().SetBasicAuth(simpleUser, simpleUserPassword)
anonynousClient := resty.R()
userprefsBaseURL := baseURL + constants.FullUserPreferencesPrefix
Convey("PutStars", t, func() {
resp, err := simpleUserClient.Put(userprefsBaseURL + PutRepoStarURL(accesibleRepo))
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
resp, err = simpleUserClient.Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(gqlStarredRepos))
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
responseStruct := StarredReposResponse{}
err = json.Unmarshal(resp.Body(), &responseStruct)
So(err, ShouldBeNil)
So(len(responseStruct.Results), ShouldEqual, 1)
So(responseStruct.Results[0].IsStarred, ShouldBeTrue)
So(responseStruct.Results[0].Name, ShouldResemble, accesibleRepo)
resp, err = anonynousClient.Put(userprefsBaseURL + PutRepoStarURL(accesibleRepo))
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusForbidden)
})
//
Convey("PutBookmark", t, func() {
resp, err := simpleUserClient.Put(userprefsBaseURL + PutRepoBookmarkURL(accesibleRepo))
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
resp, err = simpleUserClient.Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(gqlBookmarkedRepos))
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
responseStruct := BookmarkedReposResponse{}
err = json.Unmarshal(resp.Body(), &responseStruct)
So(err, ShouldBeNil)
So(err, ShouldBeNil)
So(len(responseStruct.Results), ShouldEqual, 1)
So(responseStruct.Results[0].IsBookmarked, ShouldBeTrue)
So(responseStruct.Results[0].Name, ShouldResemble, accesibleRepo)
resp, err = anonynousClient.Put(userprefsBaseURL + PutRepoBookmarkURL(accesibleRepo))
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusForbidden)
})
}
func PutRepoStarURL(repo string) string {
return fmt.Sprintf("?repo=%s&action=toggleStar", repo)
}
func PutRepoBookmarkURL(repo string) string {
return fmt.Sprintf("?repo=%s&action=toggleBookmark", repo)
}
func getCredString(username, password string) string {
hash, err := bcrypt.GenerateFromPassword([]byte(password), 10)
if err != nil {
panic(err)
}
usernameAndHash := fmt.Sprintf("%s:%s", username, string(hash))
return usernameAndHash
}