"net/url"
"regexp"
"strings"
+ "time"
"git.curoverse.com/arvados.git/lib/config"
"git.curoverse.com/arvados.git/lib/controller/localdb"
if err != nil {
return resp, err
}
- ctxRoot := auth.NewContext(ctx, &auth.Credentials{Tokens: []string{conn.cluster.SystemRootToken}})
+ batchOpts := arvados.UserBatchUpdateOptions{Updates: map[string]map[string]interface{}{}}
for _, user := range resp.Items {
if !strings.HasPrefix(user.UUID, id) {
continue
}
- logger.Debug("cache user info for uuid %q", user.UUID)
+ logger.Debugf("cache user info for uuid %q", user.UUID)
+
+ // If the remote cluster has null timestamps
+ // (e.g., test server with incomplete
+ // fixtures) use dummy timestamps (instead of
+ // the zero time, which causes a Rails API
+ // error "year too big to marshal: 1 UTC").
+ if user.ModifiedAt.IsZero() {
+ user.ModifiedAt = time.Now()
+ }
+ if user.CreatedAt.IsZero() {
+ user.CreatedAt = time.Now()
+ }
+
var allFields map[string]interface{}
buf, err := json.Marshal(user)
if err != nil {
}
}
}
- _, err = conn.local.UserUpdate(ctxRoot, arvados.UpdateOptions{
- UUID: user.UUID,
- Attrs: updates,
- })
- if errStatus(err) == http.StatusNotFound {
- updates["uuid"] = user.UUID
- _, err = conn.local.UserCreate(ctxRoot, arvados.CreateOptions{
- Attrs: updates,
- })
- }
+ batchOpts.Updates[user.UUID] = updates
+ }
+ if len(batchOpts.Updates) > 0 {
+ ctxRoot := auth.NewContext(ctx, &auth.Credentials{Tokens: []string{conn.cluster.SystemRootToken}})
+ _, err = conn.local.UserBatchUpdate(ctxRoot, batchOpts)
if err != nil {
- logger.WithError(err).WithField("UUID", user.UUID).Error("error updating local user record")
- return arvados.UserList{}, fmt.Errorf("error updating local user record: %s", err)
+ return arvados.UserList{}, fmt.Errorf("error updating local user records: %s", err)
}
}
return resp, nil
return conn.chooseBackend(options.UUID).UserDelete(ctx, options)
}
+func (conn *Conn) UserBatchUpdate(ctx context.Context, options arvados.UserBatchUpdateOptions) (arvados.UserList, error) {
+ return conn.local.UserBatchUpdate(ctx, options)
+}
+
func (conn *Conn) APIClientAuthorizationCurrent(ctx context.Context, options arvados.GetOptions) (arvados.APIClientAuthorization, error) {
return conn.chooseBackend(options.UUID).APIClientAuthorizationCurrent(ctx, options)
}
c.Check(stub.Calls(nil), check.HasLen, 0)
} else if updateFail {
c.Logf("... err %#v", err)
- calls := stub.Calls(stub.UserUpdate)
+ calls := stub.Calls(stub.UserBatchUpdate)
if c.Check(calls, check.HasLen, 1) {
c.Logf("... stub.UserUpdate called with options: %#v", calls[0].Options)
shouldUpdate := map[string]bool{
}
}
}
+ var uuid string
+ for uuid = range calls[0].Options.(arvados.UserBatchUpdateOptions).Updates {
+ }
for k, shouldFind := range shouldUpdate {
- _, found := calls[0].Options.(arvados.UpdateOptions).Attrs[k]
+ _, found := calls[0].Options.(arvados.UserBatchUpdateOptions).Updates[uuid][k]
c.Check(found, check.Equals, shouldFind, check.Commentf("offending attr: %s", k))
}
}
updates := 0
for _, d := range spy.RequestDumps {
d := string(d)
- if strings.Contains(d, "PATCH /arvados/v1/users/zzzzz-tpzed-") {
+ if strings.Contains(d, "PATCH /arvados/v1/users/batch") {
c.Check(d, check.Matches, `(?ms).*Authorization: Bearer `+arvadostest.SystemRootToken+`.*`)
updates++
}
}
c.Check(err, check.IsNil)
- c.Check(updates, check.Equals, len(userlist.Items))
+ c.Check(updates, check.Equals, 1)
c.Logf("... response items %#v", userlist.Items)
}
}
return rtr.fed.UserList(ctx, *opts.(*arvados.ListOptions))
},
},
+ {
+ arvados.EndpointUserBatchUpdate,
+ func() interface{} { return &arvados.UserBatchUpdateOptions{} },
+ func(ctx context.Context, opts interface{}) (interface{}, error) {
+ return rtr.fed.UserBatchUpdate(ctx, *opts.(*arvados.UserBatchUpdateOptions))
+ },
+ },
{
arvados.EndpointUserDelete,
func() interface{} { return &arvados.DeleteOptions{} },
err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
return resp, err
}
+
+func (conn *Conn) UserBatchUpdate(ctx context.Context, options arvados.UserBatchUpdateOptions) (arvados.UserList, error) {
+ ep := arvados.APIEndpoint{Method: "PATCH", Path: "arvados/v1/users/batch_update"}
+ var resp arvados.UserList
+ err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+ return resp, err
+}
EndpointUserUnsetup = APIEndpoint{"POST", "arvados/v1/users/{uuid}/unsetup", ""}
EndpointUserUpdate = APIEndpoint{"PATCH", "arvados/v1/users/{uuid}", "user"}
EndpointUserUpdateUUID = APIEndpoint{"POST", "arvados/v1/users/{uuid}/update_uuid", ""}
+ EndpointUserBatchUpdate = APIEndpoint{"PATCH", "arvados/v1/users/batch", ""}
EndpointAPIClientAuthorizationCurrent = APIEndpoint{"GET", "arvados/v1/api_client_authorizations/current", ""}
)
NewUserToken string `json:"new_user_token,omitempty"`
}
+type UserBatchUpdateOptions struct {
+ Updates map[string]map[string]interface{} `json:"updates"`
+}
+
+type UserBatchUpdateResponse struct{}
+
type DeleteOptions struct {
UUID string `json:"uuid"`
}
UserGetSystem(ctx context.Context, options GetOptions) (User, error)
UserList(ctx context.Context, options ListOptions) (UserList, error)
UserDelete(ctx context.Context, options DeleteOptions) (User, error)
+ UserBatchUpdate(context.Context, UserBatchUpdateOptions) (UserList, error)
APIClientAuthorizationCurrent(ctx context.Context, options GetOptions) (APIClientAuthorization, error)
}
as.appendCall(as.UserMerge, ctx, options)
return arvados.User{}, as.Error
}
+func (as *APIStub) UserBatchUpdate(ctx context.Context, options arvados.UserBatchUpdateOptions) (arvados.UserList, error) {
+ as.appendCall(as.UserBatchUpdate, ctx, options)
+ return arvados.UserList{}, as.Error
+}
func (as *APIStub) APIClientAuthorizationCurrent(ctx context.Context, options arvados.GetOptions) (arvados.APIClientAuthorization, error) {
as.appendCall(as.APIClientAuthorizationCurrent, ctx, options)
return arvados.APIClientAuthorization{}, as.Error
class Arvados::V1::UsersController < ApplicationController
accept_attribute_as_json :prefs, Hash
+ accept_param_as_json :updates
skip_before_action :find_object_by_uuid, only:
- [:activate, :current, :system, :setup, :merge]
+ [:activate, :current, :system, :setup, :merge, :batch_update]
skip_before_action :render_404_if_no_object, only:
- [:activate, :current, :system, :setup, :merge]
- before_action :admin_required, only: [:setup, :unsetup, :update_uuid]
+ [:activate, :current, :system, :setup, :merge, :batch_update]
+ before_action :admin_required, only: [:setup, :unsetup, :update_uuid, :batch_update]
+
+ # Internal API used by controller to update local cache of user
+ # records from LoginCluster.
+ def batch_update
+ @objects = []
+ params[:updates].andand.each do |uuid, attrs|
+ begin
+ u = User.find_or_create_by(uuid: uuid)
+ rescue ActiveRecord::RecordNotUnique
+ retry
+ end
+ u.update_attributes!(attrs)
+ @objects << u
+ end
+ @offset = 0
+ @limit = -1
+ render_list
+ end
def current
if current_user
post 'unsetup', on: :member
post 'update_uuid', on: :member
post 'merge', on: :collection
+ patch 'batch_update', on: :collection
end
resources :virtual_machines do
get 'logins', on: :member
assert_nil(users(:project_viewer).redirect_to_user_uuid)
end
+ test "batch update fails for non-admin" do
+ authorize_with(:active)
+ patch(:batch_update, params: {updates: {}})
+ assert_response(403)
+ end
+
+ test "batch update" do
+ existinguuid = 'remot-tpzed-foobarbazwazqux'
+ newuuid = 'remot-tpzed-newnarnazwazqux'
+ act_as_system_user do
+ User.create!(uuid: existinguuid, email: 'root@existing.example.com')
+ end
+
+ authorize_with(:admin)
+ patch(:batch_update,
+ params: {
+ updates: {
+ existinguuid => {
+ 'first_name' => 'root',
+ 'email' => 'root@remot.example.com',
+ 'is_active' => true,
+ 'is_admin' => true,
+ 'prefs' => {'foo' => 'bar'},
+ },
+ newuuid => {
+ 'first_name' => 'noot',
+ 'email' => 'root@remot.example.com',
+ },
+ }})
+ assert_response(:success)
+
+ assert_equal('root', User.find_by_uuid(existinguuid).first_name)
+ assert_equal('root@remot.example.com', User.find_by_uuid(existinguuid).email)
+ assert_equal(true, User.find_by_uuid(existinguuid).is_active)
+ assert_equal(true, User.find_by_uuid(existinguuid).is_admin)
+ assert_equal({'foo' => 'bar'}, User.find_by_uuid(existinguuid).prefs)
+
+ assert_equal('noot', User.find_by_uuid(newuuid).first_name)
+ assert_equal('root@remot.example.com', User.find_by_uuid(newuuid).email)
+ end
+
NON_ADMIN_USER_DATA = ["uuid", "kind", "is_active", "email", "first_name",
"last_name", "username"].sort