From 2b7f321f375c8a9ea986e6352dbb1f38f017367b Mon Sep 17 00:00:00 2001 From: "alessandro.pinna" Date: Mon, 14 Feb 2022 06:48:05 -0800 Subject: [PATCH] Gateway: add api user/runs and user/projectgroups --- internal/services/gateway/action/user.go | 163 ++++++++++++++++++++ internal/services/gateway/api/user.go | 107 +++++++++++++ internal/services/gateway/gateway.go | 4 + services/gateway/client/client.go | 44 ++++++ tests/setup_test.go | 182 +++++++++++++++++++++++ 5 files changed, 500 insertions(+) diff --git a/internal/services/gateway/action/user.go b/internal/services/gateway/action/user.go index 730585fa9..2667a1784 100644 --- a/internal/services/gateway/action/user.go +++ b/internal/services/gateway/action/user.go @@ -29,6 +29,7 @@ import ( "agola.io/agola/internal/util" csapitypes "agola.io/agola/services/configstore/api/types" cstypes "agola.io/agola/services/configstore/types" + rsapitypes "agola.io/agola/services/runservice/api/types" "github.com/golang-jwt/jwt/v4" errors "golang.org/x/xerrors" @@ -959,3 +960,165 @@ func (h *ActionHandler) UserCreateRun(ctx context.Context, req *UserCreateRunReq return h.CreateRuns(ctx, creq) } + +type GetUserRunsRequest struct { + PhaseFilter []string + ResultFilter []string + LastRun bool + ChangeGroups []string + StartRunID string + Limit int + Asc bool +} + +func (h *ActionHandler) GetUserRuns(ctx context.Context, req *GetUserRunsRequest) (*rsapitypes.GetRunsResponse, error) { + projectgroups, err := h.GetUserProjectgroups(ctx, &GetUserProjectgroups{ + UserType: true, + OrgType: true, + OnlyOwner: false, + }) + if err != nil { + return nil, errors.Errorf("failed to get projectgroups: %v", err) + } + + groups := make([]string, 0) + for _, pg := range projectgroups { + if pg.Parent.Type == cstypes.ConfigTypeUser { + groups = append(groups, "/user/"+pg.Parent.ID) + } + + projects, err := h.GetProjectGroupProjects(ctx, pg.Path) + if err != nil { + return nil, errors.Errorf("failed to get projectgroupprojects %s: %v", pg.Path, err) + } + + for _, p := range projects { + groups = append(groups, "/project/"+p.ID) + } + + } + + runsResp, resp, err := h.runserviceClient.GetRuns(ctx, req.PhaseFilter, req.ResultFilter, groups, req.LastRun, req.ChangeGroups, req.StartRunID, req.Limit, req.Asc) + if err != nil { + return nil, ErrFromRemote(resp, err) + } + + return runsResp, nil +} + +type GetUserProjectgroups struct { + UserType bool + OrgType bool + OnlyOwner bool +} + +func (h *ActionHandler) GetUserProjectgroups(ctx context.Context, req *GetUserProjectgroups) ([]*csapitypes.ProjectGroup, error) { + userID := "" + usernames := make([]string, 0) + + if h.IsUserAdmin(ctx) { + if req.UserType { + users, err := h.GetUsers(ctx, &GetUsersRequest{}) + if err != nil { + return nil, err + } + + for _, user := range users { + usernames = append(usernames, user.Name) + } + } + } else { + userIDVal := ctx.Value("userid") + userID = userIDVal.(string) + + user, err := h.GetUser(ctx, userID) + if err != nil { + return nil, err + } + + usernames = append(usernames, user.Name) + } + + projectgroups := make([]*csapitypes.ProjectGroup, 0) + + if req.UserType { + for _, u := range usernames { + projectgroup, err := h.GetProjectGroup(ctx, "user/"+u) + if err != nil { + return nil, err + } + projectgroups = append(projectgroups, projectgroup) + + userProjectgroups, err := h.getUserProjectgroupsSubgroups(ctx, projectgroup.Path) + if err != nil { + return nil, err + } + + projectgroups = append(projectgroups, userProjectgroups...) + } + } + + if req.OrgType { + var userOrgs []*csapitypes.UserOrgsResponse + var err error + if h.IsUserAdmin(ctx) { + orgsResponse, err := h.GetOrgs(ctx, &GetOrgsRequest{}) + if err != nil { + return nil, err + } + + userOrgs = make([]*csapitypes.UserOrgsResponse, len(orgsResponse)) + for _, o := range orgsResponse { + userOrgs = append(userOrgs, &csapitypes.UserOrgsResponse{Organization: o, Role: cstypes.MemberRoleOwner}) + } + } else { + userOrgs, err = h.GetUserOrgs(ctx, userID) + if err != nil { + return nil, err + } + } + + for _, uo := range userOrgs { + if req.OnlyOwner && uo.Role != cstypes.MemberRoleOwner { + continue + } + + projectgroup, err := h.GetProjectGroup(ctx, "org/"+uo.Organization.Name) + if err != nil { + return nil, err + } + projectgroups = append(projectgroups, projectgroup) + + orgProjectgroups, err := h.getUserProjectgroupsSubgroups(ctx, projectgroup.Path) + if err != nil { + return nil, err + } + + projectgroups = append(projectgroups, orgProjectgroups...) + } + } + + return projectgroups, nil +} + +func (h *ActionHandler) getUserProjectgroupsSubgroups(ctx context.Context, projectgroupRef string) ([]*csapitypes.ProjectGroup, error) { + projectgroups := make([]*csapitypes.ProjectGroup, 0) + + subgroups, err := h.GetProjectGroupSubgroups(ctx, projectgroupRef) + if err != nil { + return nil, err + } + + for _, subgroup := range subgroups { + projectgroups = append(projectgroups, subgroup) + + p, err := h.getUserProjectgroupsSubgroups(ctx, subgroup.Path) + if err != nil { + return nil, err + } + + projectgroups = append(projectgroups, p...) + } + + return projectgroups, nil +} diff --git a/internal/services/gateway/api/user.go b/internal/services/gateway/api/user.go index 9016b9386..628eb369e 100644 --- a/internal/services/gateway/api/user.go +++ b/internal/services/gateway/api/user.go @@ -635,3 +635,110 @@ func createUserOrgsResponse(o *csapitypes.UserOrgsResponse) *gwapitypes.UserOrgs return userOrgs } + +type UserRunsHandler struct { + log *zap.SugaredLogger + ah *action.ActionHandler +} + +func NewUserRunsHandler(logger *zap.Logger, ah *action.ActionHandler) *UserRunsHandler { + return &UserRunsHandler{log: logger.Sugar(), ah: ah} +} + +func (h *UserRunsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + q := r.URL.Query() + + phaseFilter := q["phase"] + resultFilter := q["result"] + changeGroups := q["changegroup"] + _, lastRun := q["lastrun"] + + limitS := q.Get("limit") + limit := DefaultRunsLimit + if limitS != "" { + var err error + limit, err = strconv.Atoi(limitS) + if err != nil { + httpError(w, util.NewErrBadRequest(errors.Errorf("cannot parse limit: %w", err))) + return + } + } + if limit < 0 { + httpError(w, util.NewErrBadRequest(errors.Errorf("limit must be greater or equal than 0"))) + return + } + if limit > MaxRunsLimit { + limit = MaxRunsLimit + } + asc := false + if _, ok := q["asc"]; ok { + asc = true + } + + start := q.Get("start") + + runs := make([]*gwapitypes.RunsResponse, 0) + areq := &action.GetUserRunsRequest{ + PhaseFilter: phaseFilter, + ResultFilter: resultFilter, + LastRun: lastRun, + ChangeGroups: changeGroups, + StartRunID: start, + Limit: limit, + Asc: asc, + } + runsResp, err := h.ah.GetUserRuns(ctx, areq) + if httpError(w, err) { + h.log.Errorf("err: %+v", err) + return + } + + for _, r := range runsResp.Runs { + runs = append(runs, createRunsResponse(r)) + } + + if err := httpResponse(w, http.StatusOK, runs); err != nil { + h.log.Errorf("err: %+v", err) + } +} + +type UserProjectGroupsHandler struct { + log *zap.SugaredLogger + ah *action.ActionHandler +} + +func NewUserProjectGroupsHandler(logger *zap.Logger, ah *action.ActionHandler) *UserProjectGroupsHandler { + return &UserProjectGroupsHandler{log: logger.Sugar(), ah: ah} +} + +func (h *UserProjectGroupsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + q := r.URL.Query() + + _, usertype := q["usertype"] + _, orgtype := q["orgtype"] + _, onlyowner := q["onlyowner"] + + req := &action.GetUserProjectgroups{ + UserType: usertype, + OrgType: orgtype, + OnlyOwner: onlyowner, + } + csprojectgroups, err := h.ah.GetUserProjectgroups(ctx, req) + if httpError(w, err) { + h.log.Errorf("err: %+v", err) + return + } + + subgroups := make([]*gwapitypes.ProjectGroupResponse, len(csprojectgroups)) + for i, g := range csprojectgroups { + subgroups[i] = createProjectGroupResponse(g) + } + + if err := httpResponse(w, http.StatusOK, subgroups); err != nil { + h.log.Errorf("err: %+v", err) + } +} diff --git a/internal/services/gateway/gateway.go b/internal/services/gateway/gateway.go index b9f093b1a..448a093fe 100644 --- a/internal/services/gateway/gateway.go +++ b/internal/services/gateway/gateway.go @@ -188,6 +188,8 @@ func (g *Gateway) Run(ctx context.Context) error { deleteUserHandler := api.NewDeleteUserHandler(logger, g.ah) userCreateRunHandler := api.NewUserCreateRunHandler(logger, g.ah) userOrgsHandler := api.NewUserOrgsHandler(logger, g.ah) + userRunsHandler := api.NewUserRunsHandler(logger, g.ah) + userProjectGroupsHandler := api.NewUserProjectGroupsHandler(logger, g.ah) createUserLAHandler := api.NewCreateUserLAHandler(logger, g.ah) deleteUserLAHandler := api.NewDeleteUserLAHandler(logger, g.ah) @@ -285,6 +287,8 @@ func (g *Gateway) Run(ctx context.Context) error { apirouter.Handle("/users/{userref}", authForcedHandler(deleteUserHandler)).Methods("DELETE") apirouter.Handle("/user/createrun", authForcedHandler(userCreateRunHandler)).Methods("POST") apirouter.Handle("/user/orgs", authForcedHandler(userOrgsHandler)).Methods("GET") + apirouter.Handle("/user/runs", authForcedHandler(userRunsHandler)).Methods("GET") + apirouter.Handle("/user/projectgroups", authForcedHandler(userProjectGroupsHandler)).Methods("GET") apirouter.Handle("/users/{userref}/linkedaccounts", authForcedHandler(createUserLAHandler)).Methods("POST") apirouter.Handle("/users/{userref}/linkedaccounts/{laid}", authForcedHandler(deleteUserLAHandler)).Methods("DELETE") diff --git a/services/gateway/client/client.go b/services/gateway/client/client.go index c9e2e884b..8109c3813 100644 --- a/services/gateway/client/client.go +++ b/services/gateway/client/client.go @@ -616,3 +616,47 @@ func (c *Client) GetUserOrgs(ctx context.Context) ([]*gwapitypes.UserOrgsRespons resp, err := c.getParsedResponse(ctx, "GET", "/user/orgs", nil, jsonContent, nil, &userOrgs) return userOrgs, resp, err } + +func (c *Client) GetUserProjectGroups(ctx context.Context, userType, orgType, onlyOwner bool) ([]*gwapitypes.ProjectGroupResponse, *http.Response, error) { + q := url.Values{} + + if userType { + q.Add("usertype", "") + } + if orgType { + q.Add("orgtype", "") + } + if onlyOwner { + q.Add("onlyowner", "") + } + + projectsGroup := []*gwapitypes.ProjectGroupResponse{} + resp, err := c.getParsedResponse(ctx, "GET", "/user/projectgroups", q, jsonContent, nil, &projectsGroup) + return projectsGroup, resp, err +} + +func (c *Client) GetUserRuns(ctx context.Context, phaseFilter, resultFilter, runGroups []string, start string, limit int, asc bool) ([]*gwapitypes.RunsResponse, *http.Response, error) { + q := url.Values{} + for _, phase := range phaseFilter { + q.Add("phase", phase) + } + for _, result := range resultFilter { + q.Add("result", result) + } + for _, runGroup := range runGroups { + q.Add("rungroup", runGroup) + } + if start != "" { + q.Add("start", start) + } + if limit > 0 { + q.Add("limit", strconv.Itoa(limit)) + } + if asc { + q.Add("asc", "") + } + + getRunsResponse := []*gwapitypes.RunsResponse{} + resp, err := c.getParsedResponse(ctx, "GET", "/user/runs", q, jsonContent, nil, &getRunsResponse) + return getRunsResponse, resp, err +} diff --git a/tests/setup_test.go b/tests/setup_test.go index d0e1de419..9a1edfcb0 100644 --- a/tests/setup_test.go +++ b/tests/setup_test.go @@ -37,6 +37,7 @@ import ( "agola.io/agola/internal/services/scheduler" "agola.io/agola/internal/testutil" "agola.io/agola/internal/util" + "agola.io/agola/services/gateway/api/types" gwapitypes "agola.io/agola/services/gateway/api/types" gwclient "agola.io/agola/services/gateway/client" rstypes "agola.io/agola/services/runservice/types" @@ -199,6 +200,7 @@ func setup(ctx context.Context, t *testing.T, dir string) (*testutil.TestEmbedde } agolaBinDir := os.Getenv("AGOLA_BIN_DIR") if agolaBinDir == "" { + t.Fatalf("env var AGOLA_BIN_DIR is undefined") } @@ -2000,3 +2002,183 @@ func TestUserOrgs(t *testing.T) { t.Fatalf("user orgs mismatch (-want +got):\n%s", diff) } } + +func contains(list []*gwapitypes.ProjectGroupResponse, s []string) bool { + for _, e := range s { + f := false + for _, a := range list { + if a.Path == e { + f = true + break + } + } + if !f { + return false + } + } + return true +} + +func TestGetUserProjectgroups(t *testing.T) { + dir, err := ioutil.TempDir("", "agola") + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + defer os.RemoveAll(dir) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + tetcd, tgitea, c := setup(ctx, t, dir) + defer shutdownGitea(tgitea) + defer shutdownEtcd(tetcd) + + _, token := createLinkedAccount(ctx, t, tgitea, c) + + gwClient := gwclient.NewClient(c.Gateway.APIExposedURL, token) + + //create root user projectgroup + _, _, err = gwClient.CreateProjectGroup(ctx, &gwapitypes.CreateProjectGroupRequest{ + Name: "userprojectgroup01", + ParentRef: path.Join("user", agolaUser01), + Visibility: types.VisibilityPublic, + }) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + + _, _, err = gwClient.CreateOrg(ctx, &gwapitypes.CreateOrgRequest{ + Name: "org01", + Visibility: types.VisibilityPublic}) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + + //create root org projectgroup + _, _, err = gwClient.CreateProjectGroup(ctx, &gwapitypes.CreateProjectGroupRequest{ + Name: "orgprojectgroup01", + ParentRef: path.Join("org", "org01"), + Visibility: types.VisibilityPublic, + }) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + + //Test get all projectgroups + projectgroups, _, err := gwClient.GetUserProjectGroups(ctx, true, true, true) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + if !contains(projectgroups, []string{"user/user01", "user/user01/userprojectgroup01", "org/org01", "org/org01/orgprojectgroup01"}) { + t.Fatalf("response not contains all projectgroups") + } + + //Test get usertype projectgroups + projectgroups, _, err = gwClient.GetUserProjectGroups(ctx, true, false, false) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + if !contains(projectgroups, []string{"user/user01", "user/user01/userprojectgroup01"}) { + t.Fatalf("response not contains user projectgroups") + } + + //Test get orgtype projectgroups + projectgroups, _, err = gwClient.GetUserProjectGroups(ctx, false, true, true) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + if !contains(projectgroups, []string{"org/org01", "org/org01/orgprojectgroup01"}) { + t.Fatalf("response not contains org projectgroups") + } +} + +func TestGetUserRuns(t *testing.T) { + runConfig := ` + { + runs: [ + { + name: 'run01', + tasks: [ + { + name: 'task01', + runtime: { + containers: [ + { + image: 'alpine/git', + }, + ], + }, + steps: [ + { type: 'clone' }, + { type: 'run', command: 'env' }, + ], + }, + ], + }, + ], + } + ` + + dir, err := ioutil.TempDir("", "agola") + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + defer os.RemoveAll(dir) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + tetcd, tgitea, c := setup(ctx, t, dir) + defer shutdownGitea(tgitea) + defer shutdownEtcd(tetcd) + + giteaAPIURL := fmt.Sprintf("http://%s:%s", tgitea.HTTPListenAddress, tgitea.HTTPPort) + + giteaToken, token := createLinkedAccount(ctx, t, tgitea, c) + + giteaClient := gitea.NewClient(giteaAPIURL, giteaToken) + gwClient := gwclient.NewClient(c.Gateway.APIExposedURL, token) + + giteaRepo, _ := createProject(ctx, t, giteaClient, gwClient) + + _, _, err = gwClient.CreateOrg(ctx, &gwapitypes.CreateOrgRequest{Name: "org01", Visibility: types.VisibilityPublic}) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + _, _, err = gwClient.CreateProject(ctx, &gwapitypes.CreateProjectRequest{ + Name: "project01", + ParentRef: path.Join("org", "org01"), + Visibility: gwapitypes.VisibilityPublic, + RepoPath: path.Join(giteaUser01, "repo01"), + RemoteSourceName: "gitea", + }) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + + push(t, runConfig, giteaRepo.CloneURL, giteaToken, "commit", false) + + _ = testutil.Wait(30*time.Second, func() (bool, error) { + runs, _, err := gwClient.GetUserRuns(ctx, nil, nil, nil, "", 0, false) + if err != nil { + return false, nil + } + + if len(runs) == 2 { + return true, nil + } + + return false, nil + }) + + runs, _, err := gwClient.GetUserRuns(ctx, nil, nil, nil, "", 0, false) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + + t.Logf("runs: %s", util.Dump(runs)) + + if len(runs) != 2 { + t.Fatalf("expected 2 run got: %d", len(runs)) + } +}