# HG changeset patch # User Tom Gottfried # Date 1533131378 -7200 # Node ID 750a9c9cd9652e346828521da97b3a3017331429 # Parent 70592a18c5c695b418a5d7404b029fe70f3664a5 Use SQL UPDATE to update users This implies it's not a database error anymore to try to update a non-existent user. Thus, handle this as a HTTP-404 in the backend, which is in line with what GET does. Using UPDATE here will allow to GRANT column-wise privileges. The password has become part of the view to be updatable as well. diff -r 70592a18c5c6 -r 750a9c9cd965 controllers/user.go --- a/controllers/user.go Wed Aug 01 15:18:26 2018 +0200 +++ b/controllers/user.go Wed Aug 01 15:49:38 2018 +0200 @@ -15,9 +15,14 @@ createUserExtentSQL = `SELECT sys_admin.create_user($1, $2, $3, $4, ST_MakeBox2D(ST_Point($5, $6), ST_Point($7, $8)), $9)` - updateUserSQL = `SELECT sys_admin.update_user($1, $2, $3, $4, $5, NULL, $6)` - updateUserExtentSQL = `SELECT sys_admin.update_user($1, $2, $3, $4, $5, - ST_MakeBox2D(ST_Point($6, $7), ST_Point($8, $9)), $10)` + updateUserSQL = `UPDATE users.list_users + SET (rolname, username, pw, country, map_extent, email_address) + = ($2, $3, $4, $5, NULL, $6) + WHERE username = $1` + updateUserExtentSQL = `UPDATE users.list_users + SET (rolname, username, pw, country, map_extent, email_address) + = ($2, $3, $4, $5, ST_MakeBox2D(ST_Point($6, $7), ST_Point($8, $9)), $10) + WHERE username = $1` deleteUserSQL = `SELECT sys_admin.delete_user($1)` @@ -80,9 +85,10 @@ } newUser := input.(*User) + var res sql.Result if newUser.Extent == nil { - _, err = db.Exec( + res, err = db.Exec( updateUserSQL, user, newUser.Role, @@ -92,7 +98,7 @@ newUser.Email, ) } else { - _, err = db.Exec( + res, err = db.Exec( updateUserExtentSQL, user, newUser.Role, @@ -105,6 +111,13 @@ ) } + if n, _ := res.RowsAffected(); n == 0 { + err = JSONError{ + Code: http.StatusNotFound, + Message: fmt.Sprintf("Cannot find user %s.", user), + } + return + } if err != nil { return } diff -r 70592a18c5c6 -r 750a9c9cd965 schema/auth.sql --- a/schema/auth.sql Wed Aug 01 15:18:26 2018 +0200 +++ b/schema/auth.sql Wed Aug 01 15:49:38 2018 +0200 @@ -25,7 +25,7 @@ -- Extended privileges for sys_admin -- GRANT INSERT, UPDATE, DELETE - ON users.responsibility_areas TO sys_admin; + ON users.list_users, users.responsibility_areas TO sys_admin; GRANT USAGE ON SCHEMA sys_admin TO sys_admin; GRANT SELECT ON ALL TABLES IN SCHEMA sys_admin TO sys_admin; GRANT UPDATE ON sys_admin.system_config TO sys_admin; diff -r 70592a18c5c6 -r 750a9c9cd965 schema/manage_users.sql --- a/schema/manage_users.sql Wed Aug 01 15:18:26 2018 +0200 +++ b/schema/manage_users.sql Wed Aug 01 15:49:38 2018 +0200 @@ -38,7 +38,13 @@ CREATE OR REPLACE VIEW users.list_users WITH (security_barrier) AS - SELECT r.rolname, p.* + SELECT + r.rolname, + p.username, + CAST('' AS varchar) AS pw, + p.country, + p.map_extent, + p.email_address FROM internal.user_profiles p JOIN pg_roles u ON p.username = u.rolname JOIN pg_auth_members a ON u.oid = a.member @@ -79,61 +85,52 @@ SECURITY DEFINER; -CREATE OR REPLACE FUNCTION sys_admin.update_user( - username internal.user_profiles.username%TYPE, - new_userrole varchar, - new_username internal.user_profiles.username%TYPE, - new_pw varchar, - new_country internal.user_profiles.country%TYPE, - new_map_extent internal.user_profiles.map_extent%TYPE, - new_email_address internal.user_profiles.email_address%TYPE - ) - RETURNS void +CREATE OR REPLACE FUNCTION internal.update_user() RETURNS trigger AS $$ DECLARE - cur_username name; - cur_userrole name; + cur_username varchar; BEGIN - cur_username = username; + cur_username = OLD.username; UPDATE internal.user_profiles p SET (username, country, map_extent, email_address) - = (new_username, new_country, new_map_extent, new_email_address) + = (NEW.username, NEW.country, NEW.map_extent, NEW.email_address) WHERE p.username = cur_username; - IF new_username <> cur_username + IF NEW.username <> cur_username THEN EXECUTE format( - 'ALTER ROLE %I RENAME TO %I', username, new_username); - cur_username = new_username; + 'ALTER ROLE %I RENAME TO %I', cur_username, NEW.username); + cur_username = NEW.username; END IF; - cur_userrole = rolname FROM pg_roles r - JOIN pg_auth_members a ON r.oid = a.roleid - WHERE member = ( - SELECT oid FROM pg_roles WHERE rolname = cur_username); - IF new_userrole <> cur_userrole + IF NEW.rolname <> OLD.rolname THEN EXECUTE format( - 'REVOKE %I FROM %I', cur_userrole, cur_username); + 'REVOKE %I FROM %I', OLD.rolname, cur_username); + EXECUTE format( + 'GRANT %I TO %I', NEW.rolname, cur_username); END IF; - -- GRANT new_userrole unconditionally to ensure it's an error to upgrade - -- a non-existent cur_username (GRANTing a role twice is not an error) - EXECUTE format( - 'GRANT %I TO %I', new_userrole, cur_username); - IF new_pw IS NOT NULL AND new_pw <> '' + IF NEW.pw IS NOT NULL AND NEW.pw <> '' THEN EXECUTE format( 'ALTER ROLE %I PASSWORD %L', cur_username, - users.check_password(new_pw)); + users.check_password(NEW.pw)); END IF; + + -- Do not leak new password + NEW.pw = ''; + RETURN NEW; END; $$ LANGUAGE plpgsql SECURITY DEFINER; +CREATE TRIGGER update_user INSTEAD OF UPDATE ON users.list_users FOR EACH ROW + EXECUTE PROCEDURE internal.update_user(); + CREATE OR REPLACE FUNCTION sys_admin.delete_user( username internal.user_profiles.username%TYPE diff -r 70592a18c5c6 -r 750a9c9cd965 schema/manage_users_tests.sql --- a/schema/manage_users_tests.sql Wed Aug 01 15:18:26 2018 +0200 +++ b/schema/manage_users_tests.sql Wed Aug 01 15:49:38 2018 +0200 @@ -105,74 +105,79 @@ SELECT lives_ok($$ SELECT sys_admin.create_user( 'waterway_user', 'test2', 'secret1$', 'AT', NULL, 'test2'); - SELECT sys_admin.update_user('test2', - 'waterway_user', 'test2_new', 'new_secret1$', 'AT', - (SELECT map_extent FROM users.list_users - WHERE username = 'test_user_at'), 'test5') + UPDATE users.list_users + SET (rolname, username, pw, country, map_extent, email_address) + = ('waterway_user', 'test2_new', 'new_secret1$', 'AT', + (SELECT map_extent FROM users.list_users + WHERE username = 'test_user_at'), 'test5') + WHERE username = 'test2' $$, 'Existing user can be updated'); SELECT throws_ok($$ - SELECT sys_admin.update_user('test_non_existent', - 'waterway_user', 'test_non_existent', '', 'AT', - (SELECT map_extent FROM users.list_users - WHERE username = 'test_user_at'), 'test5') - $$, - 42704, NULL, - 'Non-existent user cannot be updated'); - -SELECT throws_ok($$ - SELECT sys_admin.update_user(CAST(current_user AS varchar), - 'waterway_user', 'test_new_name', 'secret1$', 'AT', - (SELECT map_extent FROM users.list_users - WHERE username = 'test_user_at'), 'test6') + UPDATE users.list_users + SET (rolname, username, pw, country, map_extent, email_address) + = ('waterway_user', 'test_new_name', 'secret1$', 'AT', + (SELECT map_extent FROM users.list_users + WHERE username = 'test_user_at'), 'test6') + WHERE username = CAST(current_user AS varchar) $$, '0A000', NULL, 'Name of current user cannot be altered'); SELECT throws_ok($$ - SELECT sys_admin.update_user('test_user_at', - 'invalid', 'test2', 'secret1$', 'AT', - (SELECT map_extent FROM users.list_users - WHERE username = 'test_user_at'), 'test2') + UPDATE users.list_users + SET (rolname, username, pw, country, map_extent, email_address) + = ('invalid', 'test2', 'secret1$', 'AT', + (SELECT map_extent FROM users.list_users + WHERE username = 'test_user_at'), 'test2') + WHERE username = 'test_user_at' $$, 42704, NULL, 'Valid role name has to be provided'); SELECT throws_ok($$ - SELECT sys_admin.update_user('test_user_at', - 'waterway_user', NULL, 'secret1$', 'AT', - (SELECT map_extent FROM users.list_users - WHERE username = 'test_user_at'), 'test3') + UPDATE users.list_users + SET (rolname, username, pw, country, map_extent, email_address) + = ('waterway_user', NULL, 'secret1$', 'AT', + (SELECT map_extent FROM users.list_users + WHERE username = 'test_user_at'), 'test3') + WHERE username = 'test_user_at' $$, 23502, NULL, 'New username is mandatory'); -- Though other arguments are mandatory, too, there are no explicit tests SELECT throws_ok($$ - SELECT sys_admin.update_user('test_user_at', - 'waterway_user', 'waterway_user', 'secret1$', 'AT', - (SELECT map_extent FROM users.list_users - WHERE username = 'test_user_at'), 'test4') + UPDATE users.list_users + SET (rolname, username, pw, country, map_extent, email_address) + = ('waterway_user', 'waterway_user', 'secret1$', 'AT', + (SELECT map_extent FROM users.list_users + WHERE username = 'test_user_at'), 'test4') + WHERE username = 'test_user_at' $$, 42710, NULL, 'Reserved role names cannot be used as username'); SELECT throws_ok($$ - SELECT sys_admin.update_user('test_user_at', - 'waterway_user', 'test_user_ro', 'secret1$', 'AT', - (SELECT map_extent FROM users.list_users - WHERE username = 'test_user_at'), 'test4') + UPDATE users.list_users + SET (rolname, username, pw, country, map_extent, email_address) + = ('waterway_user', 'test_user_ro', 'secret1$', 'AT', + (SELECT map_extent FROM users.list_users + WHERE username = 'test_user_at'), 'test4') + WHERE username = 'test_user_at' $$, 23505, NULL, 'No duplicate user name is allowed'); -- Test password policy (only one rule to ensure it's also used on update) SELECT throws_ok($$ - SELECT sys_admin.update_user('test_user_at', - 'waterway_user', 'test_user_at', 'secret', 'AT', - (SELECT map_extent FROM users.list_users - WHERE username = 'test_user_at'), 'test4') + UPDATE users.list_users + SET (rolname, username, pw, country, map_extent, email_address) + = ('waterway_user', 'test_user_at', 'secret', 'AT', + (SELECT map_extent FROM users.list_users + WHERE username = 'test_user_at'), 'test4') + WHERE username = 'test_user_at' $$, '28P01', NULL, 'Non-compliant password is not accepted'); @@ -183,10 +188,12 @@ CREATE TEMP TABLE old_pw_hash AS SELECT rolpassword FROM pg_authid WHERE rolname = 'test_user_at'; -SELECT sys_admin.update_user('test_user_at', - 'waterway_user', 'test_user_at', NULL, 'AT', - (SELECT map_extent FROM internal.user_profiles - WHERE username = 'test_user_at'), 'xxx'); +UPDATE users.list_users + SET (rolname, username, pw, country, map_extent, email_address) + = ('waterway_user', 'test_user_at', NULL, 'AT', + (SELECT map_extent FROM internal.user_profiles + WHERE username = 'test_user_at'), 'xxx') + WHERE username = 'test_user_at'; SELECT set_eq($$ SELECT rolpassword FROM old_pw_hash $$, @@ -195,10 +202,12 @@ $$, 'Giving NULL password does not change password'); -SELECT sys_admin.update_user('test_user_at', - 'waterway_user', 'test_user_at', '', 'AT', - (SELECT map_extent FROM internal.user_profiles - WHERE username = 'test_user_at'), 'xxx'); +UPDATE users.list_users + SET (rolname, username, pw, country, map_extent, email_address) + = ('waterway_user', 'test_user_at', '', 'AT', + (SELECT map_extent FROM internal.user_profiles + WHERE username = 'test_user_at'), 'xxx') + WHERE username = 'test_user_at'; SELECT set_eq($$ SELECT rolpassword FROM old_pw_hash $$, @@ -207,10 +216,12 @@ $$, 'Giving empty string as password does not change password'); -SELECT sys_admin.update_user('test_user_at', - 'waterway_user', 'test_user_at', 'new_pw1$', 'AT', - (SELECT map_extent FROM internal.user_profiles - WHERE username = 'test_user_at'), 'xxx'); +UPDATE users.list_users + SET (rolname, username, pw, country, map_extent, email_address) + = ('waterway_user', 'test_user_at', 'new_pw1$', 'AT', + (SELECT map_extent FROM internal.user_profiles + WHERE username = 'test_user_at'), 'xxx') + WHERE username = 'test_user_at'; SELECT set_ne($$ SELECT rolpassword FROM old_pw_hash $$, diff -r 70592a18c5c6 -r 750a9c9cd965 schema/run_tests.sh --- a/schema/run_tests.sh Wed Aug 01 15:18:26 2018 +0200 +++ b/schema/run_tests.sh Wed Aug 01 15:49:38 2018 +0200 @@ -15,7 +15,7 @@ psql -qXv ON_ERROR_STOP= -v -d gemma_test \ -c "DROP ROLE IF EXISTS $TEST_ROLES" \ -f tap_tests_data.sql \ - -c 'SELECT plan(40)' \ + -c 'SELECT plan(39)' \ -f auth_tests.sql \ -f manage_users_tests.sql \ -c 'SELECT * FROM finish()'