*: implement add/update org member

This commit is contained in:
Simone Gotti 2019-05-09 16:47:22 +02:00
parent 8069063e0d
commit 620bae68df
9 changed files with 262 additions and 3 deletions

View File

@ -17,6 +17,7 @@ package action
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"time" "time"
"github.com/sorintlab/agola/internal/datamanager" "github.com/sorintlab/agola/internal/datamanager"
@ -49,12 +50,12 @@ func (h *ActionHandler) CreateOrg(ctx context.Context, org *types.Organization)
} }
// check duplicate org name // check duplicate org name
u, err := h.readDB.GetOrgByName(tx, org.Name) o, err := h.readDB.GetOrgByName(tx, org.Name)
if err != nil { if err != nil {
return err return err
} }
if u != nil { if o != nil {
return util.NewErrBadRequest(errors.Errorf("org %q already exists", u.Name)) return util.NewErrBadRequest(errors.Errorf("org %q already exists", o.Name))
} }
if org.CreatorUserID != "" { if org.CreatorUserID != "" {
@ -180,3 +181,84 @@ func (h *ActionHandler) DeleteOrg(ctx context.Context, orgRef string) error {
_, err = h.dm.WriteWal(ctx, actions, cgt) _, err = h.dm.WriteWal(ctx, actions, cgt)
return err 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
}

View File

@ -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) 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) { func (c *Client) GetOrgs(ctx context.Context, start string, limit int, asc bool) ([]*types.Organization, *http.Response, error) {
q := url.Values{} q := url.Values{}
if start != "" { if start != "" {

View File

@ -177,3 +177,41 @@ func (h *OrgsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.log.Errorf("err: %+v", err) 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)
}
}

View File

@ -163,6 +163,8 @@ func (s *Configstore) Run(ctx context.Context) error {
createOrgHandler := api.NewCreateOrgHandler(logger, s.ah) createOrgHandler := api.NewCreateOrgHandler(logger, s.ah)
deleteOrgHandler := api.NewDeleteOrgHandler(logger, s.ah) deleteOrgHandler := api.NewDeleteOrgHandler(logger, s.ah)
addOrgMemberHandler := api.NewAddOrgMemberHandler(logger, s.ah)
remoteSourceHandler := api.NewRemoteSourceHandler(logger, s.readDB) remoteSourceHandler := api.NewRemoteSourceHandler(logger, s.readDB)
remoteSourcesHandler := api.NewRemoteSourcesHandler(logger, s.readDB) remoteSourcesHandler := api.NewRemoteSourcesHandler(logger, s.readDB)
createRemoteSourceHandler := api.NewCreateRemoteSourceHandler(logger, s.ah) 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", orgsHandler).Methods("GET")
apirouter.Handle("/orgs", createOrgHandler).Methods("POST") apirouter.Handle("/orgs", createOrgHandler).Methods("POST")
apirouter.Handle("/orgs/{orgref}", deleteOrgHandler).Methods("DELETE") 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/{remotesourceref}", remoteSourceHandler).Methods("GET")
apirouter.Handle("/remotesources", remoteSourcesHandler).Methods("GET") apirouter.Handle("/remotesources", remoteSourcesHandler).Methods("GET")

View File

@ -232,6 +232,26 @@ func (r *ReadDB) deleteOrgMember(tx *db.Tx, orgmemberID string) error {
return nil 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) { func fetchOrgMembers(tx *db.Tx, q string, args ...interface{}) ([]*types.OrganizationMember, []string, error) {
rows, err := tx.Query(q, args...) rows, err := tx.Query(q, args...)
if err != nil { if err != nil {

View File

@ -100,3 +100,39 @@ func (h *ActionHandler) DeleteOrg(ctx context.Context, orgRef string) error {
} }
return nil 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
}

View File

@ -195,3 +195,56 @@ func (h *OrgsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.log.Errorf("err: %+v", err) 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)
}
}

View File

@ -188,6 +188,8 @@ func (g *Gateway) Run(ctx context.Context) error {
createOrgHandler := api.NewCreateOrgHandler(logger, g.ah) createOrgHandler := api.NewCreateOrgHandler(logger, g.ah)
deleteOrgHandler := api.NewDeleteOrgHandler(logger, g.ah) deleteOrgHandler := api.NewDeleteOrgHandler(logger, g.ah)
addOrgMemberHandler := api.NewAddOrgMemberHandler(logger, g.ah)
runHandler := api.NewRunHandler(logger, g.ah) runHandler := api.NewRunHandler(logger, g.ah)
runsHandler := api.NewRunsHandler(logger, g.ah) runsHandler := api.NewRunsHandler(logger, g.ah)
runtaskHandler := api.NewRuntaskHandler(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(orgsHandler)).Methods("GET")
apirouter.Handle("/orgs", authForcedHandler(createOrgHandler)).Methods("POST") apirouter.Handle("/orgs", authForcedHandler(createOrgHandler)).Methods("POST")
apirouter.Handle("/orgs/{orgref}", authForcedHandler(deleteOrgHandler)).Methods("DELETE") 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}", authForcedHandler(runHandler)).Methods("GET")
apirouter.Handle("/runs/{runid}/actions", authForcedHandler(runActionsHandler)).Methods("PUT") apirouter.Handle("/runs/{runid}/actions", authForcedHandler(runActionsHandler)).Methods("PUT")

View File

@ -59,6 +59,16 @@ const (
MemberRoleMember MemberRole = "member" MemberRoleMember MemberRole = "member"
) )
func IsValidMemberRole(r MemberRole) bool {
switch r {
case MemberRoleOwner:
case MemberRoleMember:
default:
return false
}
return true
}
type Parent struct { type Parent struct {
Type ConfigType `json:"type,omitempty"` Type ConfigType `json:"type,omitempty"`
ID string `json:"id,omitempty"` ID string `json:"id,omitempty"`