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.

GitblitAuthority.java 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547
  1. /*
  2. * Copyright 2012 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.authority;
  17. import java.awt.BorderLayout;
  18. import java.awt.Container;
  19. import java.awt.Dimension;
  20. import java.awt.EventQueue;
  21. import java.awt.FlowLayout;
  22. import java.awt.Insets;
  23. import java.awt.Point;
  24. import java.awt.event.ActionEvent;
  25. import java.awt.event.ActionListener;
  26. import java.awt.event.KeyAdapter;
  27. import java.awt.event.KeyEvent;
  28. import java.awt.event.WindowAdapter;
  29. import java.awt.event.WindowEvent;
  30. import java.io.BufferedInputStream;
  31. import java.io.File;
  32. import java.io.FileInputStream;
  33. import java.io.FilenameFilter;
  34. import java.io.IOException;
  35. import java.security.cert.CertificateFactory;
  36. import java.security.cert.X509Certificate;
  37. import java.text.MessageFormat;
  38. import java.util.ArrayList;
  39. import java.util.Calendar;
  40. import java.util.Collections;
  41. import java.util.Date;
  42. import java.util.HashMap;
  43. import java.util.List;
  44. import java.util.Map;
  45. import javax.activation.DataHandler;
  46. import javax.activation.FileDataSource;
  47. import javax.mail.Message;
  48. import javax.mail.Multipart;
  49. import javax.mail.internet.MimeBodyPart;
  50. import javax.mail.internet.MimeMultipart;
  51. import javax.swing.ImageIcon;
  52. import javax.swing.JFrame;
  53. import javax.swing.JLabel;
  54. import javax.swing.JOptionPane;
  55. import javax.swing.JPanel;
  56. import javax.swing.JScrollPane;
  57. import javax.swing.JSplitPane;
  58. import javax.swing.JTable;
  59. import javax.swing.JTextField;
  60. import javax.swing.RowFilter;
  61. import javax.swing.UIManager;
  62. import javax.swing.event.ListSelectionEvent;
  63. import javax.swing.event.ListSelectionListener;
  64. import javax.swing.table.TableRowSorter;
  65. import org.eclipse.jgit.errors.ConfigInvalidException;
  66. import org.eclipse.jgit.lib.StoredConfig;
  67. import org.eclipse.jgit.storage.file.FileBasedConfig;
  68. import org.eclipse.jgit.util.FS;
  69. import com.gitblit.ConfigUserService;
  70. import com.gitblit.Constants;
  71. import com.gitblit.FileSettings;
  72. import com.gitblit.IStoredSettings;
  73. import com.gitblit.IUserService;
  74. import com.gitblit.Keys;
  75. import com.gitblit.MailExecutor;
  76. import com.gitblit.client.HeaderPanel;
  77. import com.gitblit.client.Translation;
  78. import com.gitblit.models.UserModel;
  79. import com.gitblit.utils.StringUtils;
  80. import com.gitblit.utils.X509Utils;
  81. import com.gitblit.utils.X509Utils.RevocationReason;
  82. import com.gitblit.utils.X509Utils.X509Metadata;
  83. /**
  84. * Simple GUI tool for administering Gitblit client certificates.
  85. *
  86. * @author James Moger
  87. *
  88. */
  89. public class GitblitAuthority extends JFrame {
  90. private static final long serialVersionUID = 1L;
  91. private final UserCertificateTableModel tableModel;
  92. private UserCertificatePanel userCertificatePanel;
  93. private File folder;
  94. private IStoredSettings gitblitSettings;
  95. private IUserService userService;
  96. private String caKeystorePassword = null;
  97. private JTable table;
  98. private int defaultDuration;
  99. private TableRowSorter<UserCertificateTableModel> defaultSorter;
  100. public static void main(String... args) {
  101. EventQueue.invokeLater(new Runnable() {
  102. public void run() {
  103. try {
  104. UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
  105. } catch (Exception e) {
  106. }
  107. GitblitAuthority authority = new GitblitAuthority();
  108. authority.initialize();
  109. authority.setLocationRelativeTo(null);
  110. authority.setVisible(true);
  111. }
  112. });
  113. }
  114. public GitblitAuthority() {
  115. super();
  116. tableModel = new UserCertificateTableModel();
  117. defaultSorter = new TableRowSorter<UserCertificateTableModel>(tableModel);
  118. }
  119. public void initialize() {
  120. setIconImage(new ImageIcon(getClass().getResource("/gitblt-favicon.png")).getImage());
  121. setTitle("Gitblit Certificate Authority v" + Constants.VERSION + " (" + Constants.VERSION_DATE + ")");
  122. setContentPane(getUI());
  123. setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  124. addWindowListener(new WindowAdapter() {
  125. @Override
  126. public void windowClosing(WindowEvent event) {
  127. saveSizeAndPosition();
  128. }
  129. @Override
  130. public void windowOpened(WindowEvent event) {
  131. }
  132. });
  133. setSizeAndPosition();
  134. File folder = new File(System.getProperty("user.dir"));
  135. load(folder);
  136. }
  137. private void setSizeAndPosition() {
  138. String sz = null;
  139. String pos = null;
  140. try {
  141. StoredConfig config = getConfig();
  142. sz = config.getString("ui", null, "size");
  143. pos = config.getString("ui", null, "position");
  144. defaultDuration = config.getInt("new", "duration", 365);
  145. } catch (Throwable t) {
  146. t.printStackTrace();
  147. }
  148. // try to restore saved window size
  149. if (StringUtils.isEmpty(sz)) {
  150. setSize(850, 500);
  151. } else {
  152. String[] chunks = sz.split("x");
  153. int width = Integer.parseInt(chunks[0]);
  154. int height = Integer.parseInt(chunks[1]);
  155. setSize(width, height);
  156. }
  157. // try to restore saved window position
  158. if (StringUtils.isEmpty(pos)) {
  159. setLocationRelativeTo(null);
  160. } else {
  161. String[] chunks = pos.split(",");
  162. int x = Integer.parseInt(chunks[0]);
  163. int y = Integer.parseInt(chunks[1]);
  164. setLocation(x, y);
  165. }
  166. }
  167. private void saveSizeAndPosition() {
  168. try {
  169. // save window size and position
  170. StoredConfig config = getConfig();
  171. Dimension sz = GitblitAuthority.this.getSize();
  172. config.setString("ui", null, "size",
  173. MessageFormat.format("{0,number,0}x{1,number,0}", sz.width, sz.height));
  174. Point pos = GitblitAuthority.this.getLocationOnScreen();
  175. config.setString("ui", null, "position",
  176. MessageFormat.format("{0,number,0},{1,number,0}", pos.x, pos.y));
  177. config.save();
  178. } catch (Throwable t) {
  179. Utils.showException(GitblitAuthority.this, t);
  180. }
  181. }
  182. private StoredConfig getConfig() throws IOException, ConfigInvalidException {
  183. File configFile = new File(System.getProperty("user.dir"), X509Utils.CA_CONFIG);
  184. FileBasedConfig config = new FileBasedConfig(configFile, FS.detect());
  185. config.load();
  186. return config;
  187. }
  188. private IUserService loadUsers(File folder) {
  189. File file = new File(folder, "gitblit.properties");
  190. if (!file.exists()) {
  191. return null;
  192. }
  193. gitblitSettings = new FileSettings(file.getAbsolutePath());
  194. caKeystorePassword = gitblitSettings.getString(Keys.server.storePassword, null);
  195. String us = gitblitSettings.getString(Keys.realm.userService, "users.conf");
  196. String ext = us.substring(us.lastIndexOf(".") + 1).toLowerCase();
  197. IUserService service = null;
  198. if (!ext.equals("conf") && !ext.equals("properties")) {
  199. if (us.equals("com.gitblit.LdapUserService")) {
  200. us = gitblitSettings.getString(Keys.realm.ldap.backingUserService, "users.conf");
  201. } else if (us.equals("com.gitblit.LdapUserService")) {
  202. us = gitblitSettings.getString(Keys.realm.redmine.backingUserService, "users.conf");
  203. }
  204. }
  205. if (us.endsWith(".conf")) {
  206. service = new ConfigUserService(new File(us));
  207. } else {
  208. throw new RuntimeException("Unsupported user service: " + us);
  209. }
  210. service = new ConfigUserService(new File(us));
  211. return service;
  212. }
  213. private void load(File folder) {
  214. this.folder = folder;
  215. this.userService = loadUsers(folder);
  216. if (userService != null) {
  217. // build empty certificate model for all users
  218. Map<String, UserCertificateModel> map = new HashMap<String, UserCertificateModel>();
  219. for (String user : userService.getAllUsernames()) {
  220. UserModel model = userService.getUserModel(user);
  221. UserCertificateModel ucm = new UserCertificateModel(model);
  222. map.put(user, ucm);
  223. }
  224. File certificatesConfigFile = new File(folder, X509Utils.CA_CONFIG);
  225. FileBasedConfig config = new FileBasedConfig(certificatesConfigFile, FS.detect());
  226. if (certificatesConfigFile.exists()) {
  227. try {
  228. config.load();
  229. // replace user certificate model with actual data
  230. List<UserCertificateModel> list = UserCertificateConfig.KEY.parse(config).list;
  231. for (UserCertificateModel ucm : list) {
  232. ucm.user = userService.getUserModel(ucm.user.username);
  233. map.put(ucm.user.username, ucm);
  234. }
  235. } catch (IOException e) {
  236. e.printStackTrace();
  237. } catch (ConfigInvalidException e) {
  238. e.printStackTrace();
  239. }
  240. }
  241. tableModel.list = new ArrayList<UserCertificateModel>(map.values());
  242. Collections.sort(tableModel.list);
  243. tableModel.fireTableDataChanged();
  244. }
  245. }
  246. private List<X509Certificate> findCerts(File folder, String username) {
  247. List<X509Certificate> list = new ArrayList<X509Certificate>();
  248. File userFolder = new File(folder, X509Utils.CERTS + File.separator + username);
  249. if (!userFolder.exists()) {
  250. return list;
  251. }
  252. File [] certs = userFolder.listFiles(new FilenameFilter() {
  253. @Override
  254. public boolean accept(File dir, String name) {
  255. return name.toLowerCase().endsWith(".cer") || name.toLowerCase().endsWith(".crt");
  256. }
  257. });
  258. try {
  259. CertificateFactory factory = CertificateFactory.getInstance("X.509");
  260. for (File cert : certs) {
  261. BufferedInputStream is = new BufferedInputStream(new FileInputStream(cert));
  262. X509Certificate x509 = (X509Certificate) factory.generateCertificate(is);
  263. is.close();
  264. list.add(x509);
  265. }
  266. } catch (Exception e) {
  267. Utils.showException(GitblitAuthority.this, e);
  268. }
  269. return list;
  270. }
  271. private Container getUI() {
  272. userCertificatePanel = new UserCertificatePanel(this) {
  273. private static final long serialVersionUID = 1L;
  274. @Override
  275. public Insets getInsets() {
  276. return Utils.INSETS;
  277. }
  278. @Override
  279. public Date getDefaultExpiration() {
  280. Calendar c = Calendar.getInstance();
  281. c.add(Calendar.DATE, defaultDuration);
  282. c.set(Calendar.HOUR_OF_DAY, 0);
  283. c.set(Calendar.MINUTE, 0);
  284. c.set(Calendar.SECOND, 0);
  285. c.set(Calendar.MILLISECOND, 0);
  286. return c.getTime();
  287. }
  288. @Override
  289. public void saveUser(String username, UserCertificateModel ucm) {
  290. userService.updateUserModel(username, ucm.user);
  291. }
  292. @Override
  293. public void newCertificate(UserCertificateModel ucm, X509Metadata metadata, boolean sendEmail) {
  294. Date notAfter = metadata.notAfter;
  295. metadata.serverHostname = gitblitSettings.getString(Keys.web.siteName, "localhost");
  296. UserModel user = ucm.user;
  297. // set default values from config file
  298. File certificatesConfigFile = new File(folder, X509Utils.CA_CONFIG);
  299. FileBasedConfig config = new FileBasedConfig(certificatesConfigFile, FS.detect());
  300. if (certificatesConfigFile.exists()) {
  301. try {
  302. config.load();
  303. } catch (Exception e) {
  304. Utils.showException(GitblitAuthority.this, e);
  305. }
  306. NewCertificateConfig certificateConfig = NewCertificateConfig.KEY.parse(config);
  307. certificateConfig.update(metadata);
  308. }
  309. // restore expiration date
  310. metadata.notAfter = notAfter;
  311. // set user's specified OID values
  312. if (!StringUtils.isEmpty(user.organizationalUnit)) {
  313. metadata.oids.put("OU", user.organizationalUnit);
  314. }
  315. if (!StringUtils.isEmpty(user.organization)) {
  316. metadata.oids.put("O", user.organization);
  317. }
  318. if (!StringUtils.isEmpty(user.locality)) {
  319. metadata.oids.put("L", user.locality);
  320. }
  321. if (!StringUtils.isEmpty(user.stateProvince)) {
  322. metadata.oids.put("ST", user.stateProvince);
  323. }
  324. if (!StringUtils.isEmpty(user.countryCode)) {
  325. metadata.oids.put("C", user.countryCode);
  326. }
  327. File caKeystoreFile = new File(folder, X509Utils.CA_KEY_STORE);
  328. File zip = X509Utils.newClientBundle(metadata, caKeystoreFile, caKeystorePassword);
  329. // save latest expiration date
  330. if (ucm.expires == null || metadata.notAfter.after(ucm.expires)) {
  331. ucm.expires = metadata.notAfter;
  332. }
  333. ucm.update(config);
  334. try {
  335. config.save();
  336. } catch (Exception e) {
  337. Utils.showException(GitblitAuthority.this, e);
  338. }
  339. // refresh user
  340. ucm.certs = null;
  341. int modelIndex = table.convertRowIndexToModel(table.getSelectedRow());
  342. tableModel.fireTableDataChanged();
  343. table.getSelectionModel().setSelectionInterval(modelIndex, modelIndex);
  344. if (sendEmail) {
  345. // send email
  346. try {
  347. MailExecutor mail = new MailExecutor(gitblitSettings);
  348. if (mail.isReady()) {
  349. Message message = mail.createMessage(user.emailAddress);
  350. message.setSubject("Your Gitblit client certificate for " + metadata.serverHostname);
  351. // body of email
  352. String body = X509Utils.processTemplate(new File(caKeystoreFile.getParentFile(), "mail.tmpl"), metadata);
  353. if (StringUtils.isEmpty(body)) {
  354. body = MessageFormat.format("Hi {0}\n\nHere is your client certificate bundle.\nInside the zip file are installation instructions.", user.getDisplayName());
  355. }
  356. Multipart mp = new MimeMultipart();
  357. MimeBodyPart messagePart = new MimeBodyPart();
  358. messagePart.setText(body);
  359. mp.addBodyPart(messagePart);
  360. // attach zip
  361. MimeBodyPart filePart = new MimeBodyPart();
  362. FileDataSource fds = new FileDataSource(zip);
  363. filePart.setDataHandler(new DataHandler(fds));
  364. filePart.setFileName(fds.getName());
  365. mp.addBodyPart(filePart);
  366. message.setContent(mp);
  367. mail.sendNow(message);
  368. } else {
  369. JOptionPane.showMessageDialog(GitblitAuthority.this, "Sorry, the mail server settings are not configured properly.\nCan not send email.", Translation.get("gb.error"), JOptionPane.ERROR_MESSAGE);
  370. }
  371. } catch (Exception e) {
  372. Utils.showException(GitblitAuthority.this, e);
  373. }
  374. }
  375. }
  376. @Override
  377. public void revoke(UserCertificateModel ucm, X509Certificate cert, RevocationReason reason) {
  378. File caRevocationList = new File(folder, X509Utils.CA_REVOCATION_LIST);
  379. File caKeystoreFile = new File(folder, X509Utils.CA_KEY_STORE);
  380. if (X509Utils.revoke(cert, reason, caRevocationList, caKeystoreFile, caKeystorePassword)) {
  381. File certificatesConfigFile = new File(folder, X509Utils.CA_CONFIG);
  382. FileBasedConfig config = new FileBasedConfig(certificatesConfigFile, FS.detect());
  383. if (certificatesConfigFile.exists()) {
  384. try {
  385. config.load();
  386. } catch (Exception e) {
  387. Utils.showException(GitblitAuthority.this, e);
  388. }
  389. }
  390. // add serial to revoked list
  391. ucm.revoke(cert.getSerialNumber(), reason);
  392. ucm.update(config);
  393. try {
  394. config.save();
  395. } catch (Exception e) {
  396. Utils.showException(GitblitAuthority.this, e);
  397. }
  398. // refresh user
  399. ucm.certs = null;
  400. int modelIndex = table.convertRowIndexToModel(table.getSelectedRow());
  401. tableModel.fireTableDataChanged();
  402. table.getSelectionModel().setSelectionInterval(modelIndex, modelIndex);
  403. }
  404. }
  405. };
  406. table = Utils.newTable(tableModel, Utils.DATE_FORMAT);
  407. table.setRowSorter(defaultSorter);
  408. table.setDefaultRenderer(CertificateStatus.class, new CertificateStatusRenderer());
  409. table.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
  410. @Override
  411. public void valueChanged(ListSelectionEvent e) {
  412. if (e.getValueIsAdjusting()) {
  413. return;
  414. }
  415. int row = table.getSelectedRow();
  416. if (row < 0) {
  417. return;
  418. }
  419. int modelIndex = table.convertRowIndexToModel(row);
  420. UserCertificateModel ucm = tableModel.get(modelIndex);
  421. if (ucm.certs == null) {
  422. ucm.certs = findCerts(folder, ucm.user.username);
  423. }
  424. userCertificatePanel.setUserCertificateModel(ucm);
  425. }
  426. });
  427. JPanel usersPanel = new JPanel(new BorderLayout()) {
  428. private static final long serialVersionUID = 1L;
  429. @Override
  430. public Insets getInsets() {
  431. return Utils.INSETS;
  432. }
  433. };
  434. usersPanel.add(new HeaderPanel(Translation.get("gb.users"), "users_16x16.png"), BorderLayout.NORTH);
  435. usersPanel.add(new JScrollPane(table), BorderLayout.CENTER);
  436. usersPanel.setMinimumSize(new Dimension(400, 10));
  437. final JTextField filterTextfield = new JTextField(20);
  438. filterTextfield.addActionListener(new ActionListener() {
  439. public void actionPerformed(ActionEvent e) {
  440. filterUsers(filterTextfield.getText());
  441. }
  442. });
  443. filterTextfield.addKeyListener(new KeyAdapter() {
  444. public void keyReleased(KeyEvent e) {
  445. filterUsers(filterTextfield.getText());
  446. }
  447. });
  448. JPanel userControls = new JPanel(new FlowLayout(FlowLayout.RIGHT, 5, 5));
  449. userControls.add(new JLabel(Translation.get("gb.filter")));
  450. userControls.add(filterTextfield);
  451. JPanel leftPanel = new JPanel(new BorderLayout());
  452. leftPanel.add(userControls, BorderLayout.NORTH);
  453. leftPanel.add(usersPanel, BorderLayout.CENTER);
  454. userCertificatePanel.setMinimumSize(new Dimension(375, 10));
  455. JPanel root = new JPanel(new BorderLayout()) {
  456. private static final long serialVersionUID = 1L;
  457. public Insets getInsets() {
  458. return Utils.INSETS;
  459. }
  460. };
  461. JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftPanel, userCertificatePanel);
  462. splitPane.setDividerLocation(1d);
  463. root.add(splitPane);
  464. return root;
  465. }
  466. private void filterUsers(final String fragment) {
  467. if (StringUtils.isEmpty(fragment)) {
  468. table.setRowSorter(defaultSorter);
  469. return;
  470. }
  471. RowFilter<UserCertificateTableModel, Object> containsFilter = new RowFilter<UserCertificateTableModel, Object>() {
  472. public boolean include(Entry<? extends UserCertificateTableModel, ? extends Object> entry) {
  473. for (int i = entry.getValueCount() - 1; i >= 0; i--) {
  474. if (entry.getStringValue(i).toLowerCase().contains(fragment.toLowerCase())) {
  475. return true;
  476. }
  477. }
  478. return false;
  479. }
  480. };
  481. TableRowSorter<UserCertificateTableModel> sorter = new TableRowSorter<UserCertificateTableModel>(
  482. tableModel);
  483. sorter.setRowFilter(containsFilter);
  484. table.setRowSorter(sorter);
  485. }
  486. }