diff --git a/internal/services/configstore/action/org.go b/internal/services/configstore/action/org.go index f51528e..1b5f0d2 100644 --- a/internal/services/configstore/action/org.go +++ b/internal/services/configstore/action/org.go @@ -17,6 +17,7 @@ package action import ( "context" "encoding/json" + "fmt" "time" "github.com/sorintlab/agola/internal/datamanager" @@ -49,12 +50,12 @@ func (h *ActionHandler) CreateOrg(ctx context.Context, org *types.Organization) } // check duplicate org name - u, err := h.readDB.GetOrgByName(tx, org.Name) + o, err := h.readDB.GetOrgByName(tx, org.Name) if err != nil { return err } - if u != nil { - return util.NewErrBadRequest(errors.Errorf("org %q already exists", u.Name)) + if o != nil { + return util.NewErrBadRequest(errors.Errorf("org %q already exists", o.Name)) } if org.CreatorUserID != "" { @@ -180,3 +181,84 @@ func (h *ActionHandler) DeleteOrg(ctx context.Context, orgRef string) error { _, err = h.dm.WriteWal(ctx, actions, cgt) return err } + +// AddOrgMember add/updates an org member. +// TODO(sgotti) handle invitation when implemented +func (h *ActionHandler) AddOrgMember(ctx context.Context, orgRef, userRef string, role types.MemberRole) (*types.OrganizationMember, error) { + if !types.IsValidMemberRole(role) { + return nil, util.NewErrBadRequest(errors.Errorf("invalid role %q", role)) + } + + var org *types.Organization + var user *types.User + var orgmember *types.OrganizationMember + var cgt *datamanager.ChangeGroupsUpdateToken + + // must do all the checks in a single transaction to avoid concurrent changes + err := h.readDB.Do(func(tx *db.Tx) error { + var err error + // check existing org + org, err = h.readDB.GetOrg(tx, orgRef) + if err != nil { + return err + } + if org == nil { + return util.NewErrBadRequest(errors.Errorf("org %q doesn't exists", orgRef)) + } + // check existing user + user, err = h.readDB.GetUser(tx, userRef) + if err != nil { + return err + } + if user == nil { + return util.NewErrBadRequest(errors.Errorf("user %q doesn't exists", userRef)) + } + + // fetch org member if it already exist + orgmember, err = h.readDB.GetOrgMemberByOrgUserID(tx, org.ID, user.ID) + if err != nil { + return err + } + + cgNames := []string{util.EncodeSha256Hex(fmt.Sprintf("orgmember-%s-%s", org.ID, user.ID))} + cgt, err = h.readDB.GetChangeGroupsUpdateTokens(tx, cgNames) + if err != nil { + return err + } + + return nil + }) + if err != nil { + return nil, err + } + + // update if role changed + if orgmember != nil { + if orgmember.MemberRole == role { + return orgmember, nil + } + orgmember.MemberRole = role + } else { + orgmember = &types.OrganizationMember{ + ID: uuid.NewV4().String(), + OrganizationID: org.ID, + UserID: user.ID, + MemberRole: role, + } + } + + actions := []*datamanager.Action{} + orgmemberj, err := json.Marshal(orgmember) + if err != nil { + return nil, errors.Wrapf(err, "failed to marshal project group") + } + actions = append(actions, &datamanager.Action{ + ActionType: datamanager.ActionTypePut, + DataType: string(types.ConfigTypeOrgMember), + ID: orgmember.ID, + Data: orgmemberj, + }) + + _, err = h.dm.WriteWal(ctx, actions, cgt) + return orgmember, err +} diff --git a/internal/services/configstore/api/client.go b/internal/services/configstore/api/client.go index 12a129e..c396662 100644 --- a/internal/services/configstore/api/client.go +++ b/internal/services/configstore/api/client.go @@ -460,6 +460,20 @@ func (c *Client) DeleteOrg(ctx context.Context, orgRef string) (*http.Response, return c.getResponse(ctx, "DELETE", fmt.Sprintf("/orgs/%s", orgRef), nil, jsonContent, nil) } +func (c *Client) AddOrgMember(ctx context.Context, orgRef, userRef string, role types.MemberRole) (*types.OrganizationMember, *http.Response, error) { + req := &AddOrgMemberRequest{ + Role: role, + } + omj, err := json.Marshal(req) + if err != nil { + return nil, nil, err + } + + orgmember := new(types.OrganizationMember) + resp, err := c.getParsedResponse(ctx, "PUT", fmt.Sprintf("/orgs/%s/members/%s", orgRef, userRef), nil, jsonContent, bytes.NewReader(omj), orgmember) + return orgmember, resp, err +} + func (c *Client) GetOrgs(ctx context.Context, start string, limit int, asc bool) ([]*types.Organization, *http.Response, error) { q := url.Values{} if start != "" { diff --git a/internal/services/configstore/api/org.go b/internal/services/configstore/api/org.go index db113ff..6c89ec8 100644 --- a/internal/services/configstore/api/org.go +++ b/internal/services/configstore/api/org.go @@ -177,3 +177,41 @@ func (h *OrgsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.log.Errorf("err: %+v", err) } } + +type AddOrgMemberRequest struct { + Role types.MemberRole +} + +type AddOrgMemberHandler struct { + log *zap.SugaredLogger + ah *action.ActionHandler +} + +func NewAddOrgMemberHandler(logger *zap.Logger, ah *action.ActionHandler) *AddOrgMemberHandler { + return &AddOrgMemberHandler{log: logger.Sugar(), ah: ah} +} + +func (h *AddOrgMemberHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + vars := mux.Vars(r) + orgRef := vars["orgref"] + userRef := vars["userref"] + + var req AddOrgMemberRequest + d := json.NewDecoder(r.Body) + if err := d.Decode(&req); err != nil { + httpError(w, util.NewErrBadRequest(err)) + return + } + + org, err := h.ah.AddOrgMember(ctx, orgRef, userRef, req.Role) + if httpError(w, err) { + h.log.Errorf("err: %+v", err) + return + } + + if err := httpResponse(w, http.StatusCreated, org); err != nil { + h.log.Errorf("err: %+v", err) + } +} diff --git a/internal/services/configstore/configstore.go b/internal/services/configstore/configstore.go index 3e033f6..093bfc9 100644 --- a/internal/services/configstore/configstore.go +++ b/internal/services/configstore/configstore.go @@ -163,6 +163,8 @@ func (s *Configstore) Run(ctx context.Context) error { createOrgHandler := api.NewCreateOrgHandler(logger, s.ah) deleteOrgHandler := api.NewDeleteOrgHandler(logger, s.ah) + addOrgMemberHandler := api.NewAddOrgMemberHandler(logger, s.ah) + remoteSourceHandler := api.NewRemoteSourceHandler(logger, s.readDB) remoteSourcesHandler := api.NewRemoteSourcesHandler(logger, s.readDB) createRemoteSourceHandler := api.NewCreateRemoteSourceHandler(logger, s.ah) @@ -213,6 +215,7 @@ func (s *Configstore) Run(ctx context.Context) error { apirouter.Handle("/orgs", orgsHandler).Methods("GET") apirouter.Handle("/orgs", createOrgHandler).Methods("POST") apirouter.Handle("/orgs/{orgref}", deleteOrgHandler).Methods("DELETE") + apirouter.Handle("/orgs/{orgref}/members/{userref}", addOrgMemberHandler).Methods("PUT") apirouter.Handle("/remotesources/{remotesourceref}", remoteSourceHandler).Methods("GET") apirouter.Handle("/remotesources", remoteSourcesHandler).Methods("GET") diff --git a/internal/services/configstore/readdb/org.go b/internal/services/configstore/readdb/org.go index cdd987b..5433b45 100644 --- a/internal/services/configstore/readdb/org.go +++ b/internal/services/configstore/readdb/org.go @@ -232,6 +232,26 @@ func (r *ReadDB) deleteOrgMember(tx *db.Tx, orgmemberID string) error { return nil } +func (r *ReadDB) GetOrgMemberByOrgUserID(tx *db.Tx, orgID, userID string) (*types.OrganizationMember, error) { + q, args, err := orgmemberSelect.Where(sq.Eq{"orgmember.orgid": orgID, "orgmember.userid": userID}).ToSql() + r.log.Debugf("q: %s, args: %s", q, util.Dump(args)) + if err != nil { + return nil, errors.Wrap(err, "failed to build query") + } + + oms, _, err := fetchOrgMembers(tx, q, args...) + if err != nil { + return nil, errors.WithStack(err) + } + if len(oms) > 1 { + return nil, errors.Errorf("too many rows returned") + } + if len(oms) == 0 { + return nil, nil + } + return oms[0], nil +} + func fetchOrgMembers(tx *db.Tx, q string, args ...interface{}) ([]*types.OrganizationMember, []string, error) { rows, err := tx.Query(q, args...) if err != nil { diff --git a/internal/services/gateway/action/org.go b/internal/services/gateway/action/org.go index 2b8dfcb..0726d84 100644 --- a/internal/services/gateway/action/org.go +++ b/internal/services/gateway/action/org.go @@ -100,3 +100,39 @@ func (h *ActionHandler) DeleteOrg(ctx context.Context, orgRef string) error { } return nil } + +type AddOrgMemberResponse struct { + OrganizationMember *types.OrganizationMember + Org *types.Organization + User *types.User +} + +func (h *ActionHandler) AddOrgMember(ctx context.Context, orgRef, userRef string, role types.MemberRole) (*AddOrgMemberResponse, error) { + org, resp, err := h.configstoreClient.GetOrg(ctx, orgRef) + if err != nil { + return nil, ErrFromRemote(resp, err) + } + user, resp, err := h.configstoreClient.GetUser(ctx, userRef) + if err != nil { + return nil, ErrFromRemote(resp, err) + } + + isOrgOwner, err := h.IsOrgOwner(ctx, org.ID) + if err != nil { + return nil, errors.Wrapf(err, "failed to determine ownership") + } + if !isOrgOwner { + return nil, util.NewErrForbidden(errors.Errorf("user not authorized")) + } + + orgmember, resp, err := h.configstoreClient.AddOrgMember(ctx, orgRef, userRef, role) + if err != nil { + return nil, ErrFromRemote(resp, errors.Wrapf(err, "failed to add/update organization member")) + } + + return &AddOrgMemberResponse{ + OrganizationMember: orgmember, + Org: org, + User: user, + }, nil +} diff --git a/internal/services/gateway/api/org.go b/internal/services/gateway/api/org.go index 68c4b20..775d625 100644 --- a/internal/services/gateway/api/org.go +++ b/internal/services/gateway/api/org.go @@ -195,3 +195,56 @@ func (h *OrgsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.log.Errorf("err: %+v", err) } } + +type OrgMemberResponse struct { + Organization *OrgResponse `json:"organization,omitempty"` + User *UserResponse `json:"user,omitempty"` + Role types.MemberRole `json:"role,omitempty"` +} + +func createOrgMemberResponse(org *types.Organization, user *types.User, role types.MemberRole) *OrgMemberResponse { + return &OrgMemberResponse{ + Organization: createOrgResponse(org), + User: createUserResponse(user), + Role: role, + } +} + +type AddOrgMemberRequest struct { + Role types.MemberRole `json:"role"` +} + +type AddOrgMemberHandler struct { + log *zap.SugaredLogger + ah *action.ActionHandler +} + +func NewAddOrgMemberHandler(logger *zap.Logger, ah *action.ActionHandler) *AddOrgMemberHandler { + return &AddOrgMemberHandler{log: logger.Sugar(), ah: ah} +} + +func (h *AddOrgMemberHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + vars := mux.Vars(r) + orgRef := vars["orgref"] + userRef := vars["userref"] + + var req AddOrgMemberRequest + d := json.NewDecoder(r.Body) + if err := d.Decode(&req); err != nil { + httpError(w, util.NewErrBadRequest(err)) + return + } + + ares, err := h.ah.AddOrgMember(ctx, orgRef, userRef, req.Role) + if httpError(w, err) { + h.log.Errorf("err: %+v", err) + return + } + + res := createOrgMemberResponse(ares.Org, ares.User, ares.OrganizationMember.MemberRole) + if err := httpResponse(w, http.StatusOK, res); err != nil { + h.log.Errorf("err: %+v", err) + } +} diff --git a/internal/services/gateway/gateway.go b/internal/services/gateway/gateway.go index b91927e..3f3055b 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 { createOrgHandler := api.NewCreateOrgHandler(logger, g.ah) deleteOrgHandler := api.NewDeleteOrgHandler(logger, g.ah) + addOrgMemberHandler := api.NewAddOrgMemberHandler(logger, g.ah) + runHandler := api.NewRunHandler(logger, g.ah) runsHandler := api.NewRunsHandler(logger, g.ah) runtaskHandler := api.NewRuntaskHandler(logger, g.ah) @@ -261,6 +263,7 @@ func (g *Gateway) Run(ctx context.Context) error { apirouter.Handle("/orgs", authForcedHandler(orgsHandler)).Methods("GET") apirouter.Handle("/orgs", authForcedHandler(createOrgHandler)).Methods("POST") apirouter.Handle("/orgs/{orgref}", authForcedHandler(deleteOrgHandler)).Methods("DELETE") + apirouter.Handle("/orgs/{orgref}/members/{userref}", authForcedHandler(addOrgMemberHandler)).Methods("PUT") apirouter.Handle("/runs/{runid}", authForcedHandler(runHandler)).Methods("GET") apirouter.Handle("/runs/{runid}/actions", authForcedHandler(runActionsHandler)).Methods("PUT") diff --git a/internal/services/types/types.go b/internal/services/types/types.go index 898a97e..56b4d9e 100644 --- a/internal/services/types/types.go +++ b/internal/services/types/types.go @@ -59,6 +59,16 @@ const ( MemberRoleMember MemberRole = "member" ) +func IsValidMemberRole(r MemberRole) bool { + switch r { + case MemberRoleOwner: + case MemberRoleMember: + default: + return false + } + return true +} + type Parent struct { Type ConfigType `json:"type,omitempty"` ID string `json:"id,omitempty"`