You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

UsersDispatcher.java 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589
  1. /*
  2. * Copyright 2014 gitblit.com.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. package com.gitblit.transport.ssh.gitblit;
  17. import java.util.ArrayList;
  18. import java.util.List;
  19. import org.kohsuke.args4j.Argument;
  20. import org.kohsuke.args4j.Option;
  21. import com.gitblit.Constants.AccessPermission;
  22. import com.gitblit.GitBlitException;
  23. import com.gitblit.Keys;
  24. import com.gitblit.manager.IGitblit;
  25. import com.gitblit.models.RegistrantAccessPermission;
  26. import com.gitblit.models.RepositoryModel;
  27. import com.gitblit.models.UserModel;
  28. import com.gitblit.transport.ssh.SshKey;
  29. import com.gitblit.transport.ssh.commands.CommandMetaData;
  30. import com.gitblit.transport.ssh.commands.DispatchCommand;
  31. import com.gitblit.transport.ssh.commands.ListFilterCommand;
  32. import com.gitblit.transport.ssh.commands.SshCommand;
  33. import com.gitblit.transport.ssh.commands.UsageExample;
  34. import com.gitblit.transport.ssh.commands.UsageExamples;
  35. import com.gitblit.utils.ArrayUtils;
  36. import com.gitblit.utils.FlipTable;
  37. import com.gitblit.utils.FlipTable.Borders;
  38. import com.gitblit.utils.StringUtils;
  39. import com.google.common.base.Joiner;
  40. @CommandMetaData(name = "users", description = "User management commands", admin = true)
  41. public class UsersDispatcher extends DispatchCommand {
  42. @Override
  43. protected void setup(UserModel user) {
  44. // primary user commands
  45. register(user, NewUser.class);
  46. register(user, RenameUser.class);
  47. register(user, RemoveUser.class);
  48. register(user, ShowUser.class);
  49. register(user, ListUsers.class);
  50. // user-specific commands
  51. register(user, SetField.class);
  52. register(user, Permissions.class);
  53. register(user, DisableUser.class);
  54. register(user, EnableUser.class);
  55. }
  56. public static abstract class UserCommand extends SshCommand {
  57. @Argument(index = 0, required = true, metaVar = "USERNAME", usage = "username")
  58. protected String username;
  59. protected UserModel getUser(boolean requireUser) throws UnloggedFailure {
  60. IGitblit gitblit = getContext().getGitblit();
  61. UserModel user = gitblit.getUserModel(username);
  62. if (requireUser && user == null) {
  63. throw new UnloggedFailure(1, String.format("User %s does not exist!", username));
  64. }
  65. return user;
  66. }
  67. }
  68. @CommandMetaData(name = "new", description = "Create a new user account")
  69. @UsageExample(syntax = "${cmd} john 12345 --email john@smith.com --canFork --canCreate")
  70. public static class NewUser extends UserCommand {
  71. @Argument(index = 1, required = true, metaVar = "PASSWORD", usage = "password")
  72. protected String password;
  73. @Option(name = "--email", metaVar = "ADDRESS", usage = "email address")
  74. protected String email;
  75. @Option(name = "--canAdmin", usage = "can administer the server")
  76. protected boolean canAdmin;
  77. @Option(name = "--canFork", usage = "can fork repositories")
  78. protected boolean canFork;
  79. @Option(name = "--canCreate", usage = "can create personal repositories")
  80. protected boolean canCreate;
  81. @Option(name = "--disabled", usage = "create a disabled user account")
  82. protected boolean disabled;
  83. @Override
  84. public void run() throws UnloggedFailure {
  85. if (getUser(false) != null) {
  86. throw new UnloggedFailure(1, String.format("User %s already exists!", username));
  87. }
  88. UserModel user = new UserModel(username);
  89. user.password = password;
  90. if (email != null) {
  91. user.emailAddress = email;
  92. }
  93. user.canAdmin = canAdmin;
  94. user.canFork = canFork;
  95. user.canCreate = canCreate;
  96. user.disabled = disabled;
  97. IGitblit gitblit = getContext().getGitblit();
  98. try {
  99. gitblit.addUser(user);
  100. stdout.println(String.format("%s created.", username));
  101. } catch (GitBlitException e) {
  102. log.error("Failed to add " + username, e);
  103. throw new UnloggedFailure(1, e.getMessage());
  104. }
  105. }
  106. }
  107. @CommandMetaData(name = "rename", aliases = { "mv" }, description = "Rename an account")
  108. @UsageExample(syntax = "${cmd} john frank", description = "Rename the account from john to frank")
  109. public static class RenameUser extends UserCommand {
  110. @Argument(index = 1, required = true, metaVar = "NEWNAME", usage = "the new account name")
  111. protected String newUserName;
  112. @Override
  113. public void run() throws UnloggedFailure {
  114. UserModel user = getUser(true);
  115. IGitblit gitblit = getContext().getGitblit();
  116. if (null != gitblit.getTeamModel(newUserName)) {
  117. throw new UnloggedFailure(1, String.format("Team %s already exists!", newUserName));
  118. }
  119. // set the new name
  120. user.username = newUserName;
  121. try {
  122. gitblit.reviseUser(username, user);
  123. stdout.println(String.format("Renamed user %s to %s.", username, newUserName));
  124. } catch (GitBlitException e) {
  125. String msg = String.format("Failed to rename user from %s to %s", username, newUserName);
  126. log.error(msg, e);
  127. throw new UnloggedFailure(1, msg);
  128. }
  129. }
  130. }
  131. @CommandMetaData(name = "set", description = "Set the specified field of an account")
  132. @UsageExample(syntax = "${cmd} john name John Smith", description = "Set the display name to \"John Smith\" for john's account")
  133. public static class SetField extends UserCommand {
  134. @Argument(index = 1, required = true, metaVar = "FIELD", usage = "the field to update")
  135. protected String fieldName;
  136. @Argument(index = 2, required = true, metaVar = "VALUE", usage = "the new value")
  137. protected List<String> fieldValues = new ArrayList<String>();
  138. protected enum Field {
  139. name, displayName, email, password, canAdmin, canFork, canCreate;
  140. static Field fromString(String name) {
  141. for (Field field : values()) {
  142. if (field.name().equalsIgnoreCase(name)) {
  143. return field;
  144. }
  145. }
  146. return null;
  147. }
  148. }
  149. @Override
  150. protected String getUsageText() {
  151. String fields = Joiner.on(", ").join(Field.values());
  152. StringBuilder sb = new StringBuilder();
  153. sb.append("Valid fields are:\n ").append(fields);
  154. return sb.toString();
  155. }
  156. @Override
  157. public void run() throws UnloggedFailure {
  158. UserModel user = getUser(true);
  159. Field field = Field.fromString(fieldName);
  160. if (field == null) {
  161. throw new UnloggedFailure(1, String.format("Unknown field %s", fieldName));
  162. }
  163. String value = Joiner.on(" ").join(fieldValues).trim();
  164. IGitblit gitblit = getContext().getGitblit();
  165. boolean editCredentials = gitblit.supportsCredentialChanges(user);
  166. boolean editDisplayName = gitblit.supportsDisplayNameChanges(user);
  167. boolean editEmailAddress = gitblit.supportsEmailAddressChanges(user);
  168. String m = String.format("Can not edit %s for %s (%s)", field, user.username, user.accountType);
  169. switch(field) {
  170. case name:
  171. case displayName:
  172. if (!editDisplayName) {
  173. throw new UnloggedFailure(1, m);
  174. }
  175. user.displayName = value;
  176. break;
  177. case email:
  178. if (!editEmailAddress) {
  179. throw new UnloggedFailure(1, m);
  180. }
  181. user.emailAddress = value;
  182. break;
  183. case password:
  184. if (!editCredentials) {
  185. throw new UnloggedFailure(1, m);
  186. }
  187. int minLength = gitblit.getSettings().getInteger(Keys.realm.minPasswordLength, 5);
  188. if (minLength < 4) {
  189. minLength = 4;
  190. }
  191. if (value.trim().length() < minLength) {
  192. throw new UnloggedFailure(1, "Password is too short.");
  193. }
  194. // Optionally store the password MD5 digest.
  195. String type = gitblit.getSettings().getString(Keys.realm.passwordStorage, "md5");
  196. if (type.equalsIgnoreCase("md5")) {
  197. // store MD5 digest of password
  198. user.password = StringUtils.MD5_TYPE + StringUtils.getMD5(value);
  199. } else if (type.equalsIgnoreCase("combined-md5")) {
  200. // store MD5 digest of username+password
  201. user.password = StringUtils.COMBINED_MD5_TYPE + StringUtils.getMD5(username + value);
  202. } else {
  203. user.password = value;
  204. }
  205. // reset the cookie
  206. user.cookie = StringUtils.getSHA1(user.username + value);
  207. break;
  208. case canAdmin:
  209. user.canAdmin = toBool(value);
  210. break;
  211. case canFork:
  212. user.canFork = toBool(value);
  213. break;
  214. case canCreate:
  215. user.canCreate = toBool(value);
  216. break;
  217. default:
  218. throw new UnloggedFailure(1, String.format("Field %s was not properly handled by the set command.", fieldName));
  219. }
  220. try {
  221. gitblit.reviseUser(username, user);
  222. stdout.println(String.format("Set %s.%s = %s", username, fieldName, value));
  223. } catch (GitBlitException e) {
  224. String msg = String.format("Failed to set %s.%s = %s", username, fieldName, value);
  225. log.error(msg, e);
  226. throw new UnloggedFailure(1, msg);
  227. }
  228. }
  229. protected boolean toBool(String value) throws UnloggedFailure {
  230. String v = value.toLowerCase();
  231. if (v.equals("t")
  232. || v.equals("true")
  233. || v.equals("yes")
  234. || v.equals("on")
  235. || v.equals("y")
  236. || v.equals("1")) {
  237. return true;
  238. } else if (v.equals("f")
  239. || v.equals("false")
  240. || v.equals("no")
  241. || v.equals("off")
  242. || v.equals("n")
  243. || v.equals("0")) {
  244. return false;
  245. }
  246. throw new UnloggedFailure(1, String.format("Invalid boolean value %s", value));
  247. }
  248. }
  249. @CommandMetaData(name = "disable", description = "Prohibit an account from authenticating")
  250. @UsageExample(syntax = "${cmd} john", description = "Prevent John from authenticating")
  251. public static class DisableUser extends UserCommand {
  252. @Override
  253. public void run() throws UnloggedFailure {
  254. UserModel user = getUser(true);
  255. user.disabled = true;
  256. IGitblit gitblit = getContext().getGitblit();
  257. if (gitblit.updateUserModel(username, user)) {
  258. stdout.println(String.format("%s is not allowed to authenticate.", username));
  259. } else {
  260. throw new UnloggedFailure(1, String.format("Failed to disable %s!", username));
  261. }
  262. }
  263. }
  264. @CommandMetaData(name = "enable", description = "Allow an account to authenticate")
  265. @UsageExample(syntax = "${cmd} john", description = "Allow John to authenticate")
  266. public static class EnableUser extends UserCommand {
  267. @Override
  268. public void run() throws UnloggedFailure {
  269. UserModel user = getUser(true);
  270. user.disabled = false;
  271. IGitblit gitblit = getContext().getGitblit();
  272. if (gitblit.updateUserModel(username, user)) {
  273. stdout.println(String.format("%s may now authenticate.", username));
  274. } else {
  275. throw new UnloggedFailure(1, String.format("Failed to enable %s!", username));
  276. }
  277. }
  278. }
  279. @CommandMetaData(name = "permissions", aliases = { "perms" }, description = "Add or remove permissions from an account")
  280. @UsageExample(syntax = "${cmd} john RW:alpha/repo.git RWC:alpha/repo2.git", description = "Add or set permissions for John")
  281. public static class Permissions extends UserCommand {
  282. @Argument(index = 1, multiValued = true, metaVar = "[PERMISSION:]REPOSITORY", usage = "a repository expression")
  283. protected List<String> permissions;
  284. @Option(name = "--remove", aliases = { "-r" }, metaVar = "REPOSITORY|ALL", usage = "remove a repository permission")
  285. protected List<String> removals;
  286. @Override
  287. public void run() throws UnloggedFailure {
  288. IGitblit gitblit = getContext().getGitblit();
  289. UserModel user = getUser(true);
  290. boolean modified = false;
  291. if (!ArrayUtils.isEmpty(removals)) {
  292. if (removals.contains("ALL")) {
  293. user.permissions.clear();
  294. } else {
  295. for (String repo : removals) {
  296. user.removeRepositoryPermission(repo);
  297. log.info(String.format("Removing permission for %s from %s", repo, username));
  298. }
  299. }
  300. modified = true;
  301. }
  302. if (!ArrayUtils.isEmpty(permissions)) {
  303. for (String perm : permissions) {
  304. String repo = AccessPermission.repositoryFromRole(perm);
  305. if (StringUtils.findInvalidCharacter(repo) == null) {
  306. // explicit permision, confirm repository
  307. RepositoryModel r = gitblit.getRepositoryModel(repo);
  308. if (r == null) {
  309. throw new UnloggedFailure(1, String.format("Repository %s does not exist!", repo));
  310. }
  311. }
  312. AccessPermission ap = AccessPermission.permissionFromRole(perm);
  313. user.setRepositoryPermission(repo, ap);
  314. log.info(String.format("Setting %s:%s for %s", ap.name(), repo, username));
  315. }
  316. modified = true;
  317. }
  318. if (modified && gitblit.updateUserModel(username, user)) {
  319. // reload & display new permissions
  320. user = gitblit.getUserModel(username);
  321. }
  322. showPermissions(user);
  323. }
  324. protected void showPermissions(UserModel user) {
  325. List<RegistrantAccessPermission> perms = user.getRepositoryPermissions();
  326. String[] pheaders = { "Repository", "Permission", "Type", "Source", "Mutable" };
  327. Object [][] pdata = new Object[perms.size()][];
  328. for (int i = 0; i < perms.size(); i++) {
  329. RegistrantAccessPermission ap = perms.get(i);
  330. pdata[i] = new Object[] { ap.registrant, ap.permission, ap.permissionType, ap.source, ap.mutable ? "Y":"" };
  331. }
  332. stdout.println(FlipTable.of(pheaders, pdata, Borders.BODY_HCOLS));
  333. }
  334. }
  335. @CommandMetaData(name = "remove", aliases = { "rm" }, description = "Remove a user account")
  336. @UsageExample(syntax = "${cmd} john", description = "Delete john's account")
  337. public static class RemoveUser extends UserCommand {
  338. @Override
  339. public void run() throws UnloggedFailure {
  340. UserModel user = getUser(true);
  341. IGitblit gitblit = getContext().getGitblit();
  342. if (gitblit.deleteUserModel(user)) {
  343. stdout.println(String.format("%s has been deleted.", username));
  344. } else {
  345. throw new UnloggedFailure(1, String.format("Failed to delete %s!", username));
  346. }
  347. }
  348. }
  349. @CommandMetaData(name = "show", description = "Show the details of an account")
  350. @UsageExample(syntax = "${cmd} john", description = "Display john's account")
  351. public static class ShowUser extends UserCommand {
  352. @Override
  353. public void run() throws UnloggedFailure {
  354. UserModel u = getUser(true);
  355. // fields
  356. StringBuilder fb = new StringBuilder();
  357. fb.append("Email : ").append(u.emailAddress == null ? "": u.emailAddress).append('\n');
  358. fb.append("Type : ").append(u.accountType).append('\n');
  359. fb.append("Can Admin : ").append(u.canAdmin() ? "Y":"").append('\n');
  360. fb.append("Can Fork : ").append(u.canFork() ? "Y":"").append('\n');
  361. fb.append("Can Create : ").append(u.canCreate() ? "Y":"").append('\n');
  362. String fields = fb.toString();
  363. // teams
  364. String teams;
  365. if (u.teams.size() == 0) {
  366. teams = FlipTable.EMPTY;
  367. } else {
  368. teams = Joiner.on(", ").join(u.teams);
  369. }
  370. // owned repositories
  371. String ownedTable;
  372. List<RepositoryModel> owned = new ArrayList<RepositoryModel>();
  373. for (RepositoryModel repository : getContext().getGitblit().getRepositoryModels(u)) {
  374. if (repository.isOwner(u.username)) {
  375. owned.add(repository);
  376. }
  377. }
  378. if (owned.isEmpty()) {
  379. ownedTable = FlipTable.EMPTY;
  380. } else {
  381. String [] theaders = new String [] { "Repository", "Description" };
  382. Object [][] tdata = new Object[owned.size()][];
  383. int i = 0;
  384. for (RepositoryModel r : owned) {
  385. tdata[i] = new Object [] { r.name, r.description };
  386. i++;
  387. }
  388. ownedTable = FlipTable.of(theaders, tdata, Borders.COLS);
  389. }
  390. // permissions
  391. List<RegistrantAccessPermission> perms = u.getRepositoryPermissions();
  392. String permissions;
  393. if (perms.isEmpty()) {
  394. permissions = FlipTable.EMPTY;
  395. } else {
  396. String[] pheaders = { "Repository", "Permission", "Type", "Source", "Mutable" };
  397. Object [][] pdata = new Object[perms.size()][];
  398. for (int i = 0; i < perms.size(); i++) {
  399. RegistrantAccessPermission ap = perms.get(i);
  400. pdata[i] = new Object[] { ap.registrant, ap.permission, ap.permissionType, ap.source, ap.mutable ? "Y":"" };
  401. }
  402. permissions = FlipTable.of(pheaders, pdata, Borders.COLS);
  403. }
  404. // keys
  405. String keyTable;
  406. List<SshKey> keys = getContext().getGitblit().getPublicKeyManager().getKeys(u.username);
  407. if (ArrayUtils.isEmpty(keys)) {
  408. keyTable = FlipTable.EMPTY;
  409. } else {
  410. String[] headers = { "#", "Fingerprint", "Comment", "Type" };
  411. int len = keys == null ? 0 : keys.size();
  412. Object[][] data = new Object[len][];
  413. for (int i = 0; i < len; i++) {
  414. // show 1-based index numbers with the fingerprint
  415. // this is useful for comparing with "ssh-add -l"
  416. SshKey k = keys.get(i);
  417. data[i] = new Object[] { (i + 1), k.getFingerprint(), k.getComment(), k.getAlgorithm() };
  418. }
  419. keyTable = FlipTable.of(headers, data, Borders.COLS);
  420. }
  421. // assemble user table
  422. String userTitle = u.getDisplayName() + (u.username.equals(u.getDisplayName()) ? "" : (" (" + u.username + ")"));
  423. if (u.disabled) {
  424. userTitle += " [DISABLED]";
  425. }
  426. String [] headers = new String[] { userTitle };
  427. String[][] data = new String[8][];
  428. data[0] = new String [] { "FIELDS" };
  429. data[1] = new String [] { fields };
  430. data[2] = new String [] { "TEAMS" };
  431. data[3] = new String [] { teams };
  432. data[4] = new String [] { "OWNED REPOSITORIES" };
  433. data[5] = new String [] { ownedTable };
  434. data[4] = new String [] { "PERMISSIONS" };
  435. data[5] = new String [] { permissions };
  436. data[6] = new String [] { "SSH PUBLIC KEYS" };
  437. data[7] = new String [] { keyTable };
  438. stdout.println(FlipTable.of(headers, data));
  439. }
  440. }
  441. @CommandMetaData(name = "list", aliases= { "ls" }, description = "List accounts")
  442. @UsageExamples(examples = {
  443. @UsageExample(syntax = "${cmd}", description = "List accounts as a table"),
  444. @UsageExample(syntax = "${cmd} j.*", description = "List all accounts that start with 'j'"),
  445. })
  446. public static class ListUsers extends ListFilterCommand<UserModel> {
  447. @Override
  448. protected List<UserModel> getItems() {
  449. IGitblit gitblit = getContext().getGitblit();
  450. List<UserModel> users = gitblit.getAllUsers();
  451. return users;
  452. }
  453. @Override
  454. protected boolean matches(String filter, UserModel u) {
  455. return u.username.matches(filter);
  456. }
  457. @Override
  458. protected void asTable(List<UserModel> list) {
  459. String[] headers;
  460. if (verbose) {
  461. String[] h = { "Name", "Display name", "Email", "Type", "Teams", "Create?", "Fork?"};
  462. headers = h;
  463. } else {
  464. String[] h = { "Name", "Display name", "Email", "Type"};
  465. headers = h;
  466. }
  467. Object[][] data = new Object[list.size()][];
  468. for (int i = 0; i < list.size(); i++) {
  469. UserModel u = list.get(i);
  470. String name = (u.disabled ? "-" : ((u.canAdmin() ? "*" : " "))) + u.username;
  471. if (verbose) {
  472. data[i] = new Object[] {
  473. name,
  474. u.displayName,
  475. u.emailAddress,
  476. u.accountType + (u.canAdmin() ? ",admin":""),
  477. u.teams.isEmpty() ? "" : u.teams.size(),
  478. (u.canAdmin() || u.canCreate()) ? "Y":"",
  479. (u.canAdmin() || u.canFork()) ? "Y" : ""};
  480. } else {
  481. data[i] = new Object[] {
  482. name,
  483. u.displayName,
  484. u.emailAddress,
  485. u.accountType + (u.canAdmin() ? ",admin":"")};
  486. }
  487. }
  488. stdout.print(FlipTable.of(headers, data, Borders.BODY_HCOLS));
  489. stdout.println(" * = admin account, - = disabled account");
  490. stdout.println();
  491. }
  492. @Override
  493. protected void asTabbed(List<UserModel> users) {
  494. if (verbose) {
  495. for (UserModel u : users) {
  496. outTabbed(
  497. u.disabled ? "-" : ((u.canAdmin() ? "*" : " ")) + u.username,
  498. u.getDisplayName(),
  499. u.emailAddress == null ? "" : u.emailAddress,
  500. u.accountType + (u.canAdmin() ? ",admin":""),
  501. u.teams.isEmpty() ? "" : u.teams.size(),
  502. (u.canAdmin() || u.canCreate()) ? "Y":"",
  503. (u.canAdmin() || u.canFork()) ? "Y" : "");
  504. }
  505. } else {
  506. for (UserModel u : users) {
  507. outTabbed(u.disabled ? "-" : ((u.canAdmin() ? "*" : " ")) + u.username);
  508. }
  509. }
  510. }
  511. }
  512. }