--- /dev/null
+/*\r
+ * Copyright 2012 gitblit.com.\r
+ *\r
+ * Licensed under the Apache License, Version 2.0 (the "License");\r
+ * you may not use this file except in compliance with the License.\r
+ * You may obtain a copy of the License at\r
+ *\r
+ * http://www.apache.org/licenses/LICENSE-2.0\r
+ *\r
+ * Unless required by applicable law or agreed to in writing, software\r
+ * distributed under the License is distributed on an "AS IS" BASIS,\r
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ * See the License for the specific language governing permissions and\r
+ * limitations under the License.\r
+ */\r
+package com.gitblit.authority;\r
+\r
+import java.awt.BorderLayout;\r
+import java.awt.Container;\r
+import java.awt.Dimension;\r
+import java.awt.EventQueue;\r
+import java.awt.FlowLayout;\r
+import java.awt.Insets;\r
+import java.awt.Point;\r
+import java.awt.event.ActionEvent;\r
+import java.awt.event.ActionListener;\r
+import java.awt.event.KeyAdapter;\r
+import java.awt.event.KeyEvent;\r
+import java.awt.event.WindowAdapter;\r
+import java.awt.event.WindowEvent;\r
+import java.io.BufferedInputStream;\r
+import java.io.File;\r
+import java.io.FileInputStream;\r
+import java.io.FilenameFilter;\r
+import java.io.IOException;\r
+import java.security.cert.CertificateFactory;\r
+import java.security.cert.X509Certificate;\r
+import java.text.MessageFormat;\r
+import java.util.ArrayList;\r
+import java.util.Calendar;\r
+import java.util.Collections;\r
+import java.util.Date;\r
+import java.util.HashMap;\r
+import java.util.List;\r
+import java.util.Map;\r
+\r
+import javax.activation.DataHandler;\r
+import javax.activation.FileDataSource;\r
+import javax.mail.Message;\r
+import javax.mail.Multipart;\r
+import javax.mail.internet.MimeBodyPart;\r
+import javax.mail.internet.MimeMultipart;\r
+import javax.swing.ImageIcon;\r
+import javax.swing.JFrame;\r
+import javax.swing.JLabel;\r
+import javax.swing.JOptionPane;\r
+import javax.swing.JPanel;\r
+import javax.swing.JScrollPane;\r
+import javax.swing.JSplitPane;\r
+import javax.swing.JTable;\r
+import javax.swing.JTextField;\r
+import javax.swing.RowFilter;\r
+import javax.swing.UIManager;\r
+import javax.swing.event.ListSelectionEvent;\r
+import javax.swing.event.ListSelectionListener;\r
+import javax.swing.table.TableRowSorter;\r
+\r
+import org.eclipse.jgit.errors.ConfigInvalidException;\r
+import org.eclipse.jgit.lib.StoredConfig;\r
+import org.eclipse.jgit.storage.file.FileBasedConfig;\r
+import org.eclipse.jgit.util.FS;\r
+\r
+import com.gitblit.ConfigUserService;\r
+import com.gitblit.Constants;\r
+import com.gitblit.FileSettings;\r
+import com.gitblit.IStoredSettings;\r
+import com.gitblit.IUserService;\r
+import com.gitblit.Keys;\r
+import com.gitblit.MailExecutor;\r
+import com.gitblit.client.HeaderPanel;\r
+import com.gitblit.client.Translation;\r
+import com.gitblit.models.UserModel;\r
+import com.gitblit.utils.StringUtils;\r
+import com.gitblit.utils.X509Utils;\r
+import com.gitblit.utils.X509Utils.RevocationReason;\r
+import com.gitblit.utils.X509Utils.X509Metadata;\r
+\r
+/**\r
+ * Simple GUI tool for administering Gitblit client certificates.\r
+ * \r
+ * @author James Moger\r
+ *\r
+ */\r
+public class GitblitAuthority extends JFrame {\r
+\r
+ private static final long serialVersionUID = 1L;\r
+ \r
+ private final UserCertificateTableModel tableModel;\r
+\r
+ private UserCertificatePanel userCertificatePanel;\r
+ \r
+ private File folder;\r
+ \r
+ private IStoredSettings gitblitSettings;\r
+ \r
+ private IUserService userService;\r
+ \r
+ private String caKeystorePassword = null;\r
+\r
+ private JTable table;\r
+ \r
+ private int defaultDuration;\r
+ \r
+ private TableRowSorter<UserCertificateTableModel> defaultSorter;\r
+\r
+ public static void main(String... args) {\r
+ EventQueue.invokeLater(new Runnable() {\r
+ public void run() {\r
+ try {\r
+ UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());\r
+ } catch (Exception e) {\r
+ }\r
+ GitblitAuthority authority = new GitblitAuthority();\r
+ authority.initialize();\r
+ authority.setLocationRelativeTo(null);\r
+ authority.setVisible(true);\r
+ }\r
+ });\r
+ }\r
+\r
+ public GitblitAuthority() {\r
+ super();\r
+ tableModel = new UserCertificateTableModel();\r
+ defaultSorter = new TableRowSorter<UserCertificateTableModel>(tableModel);\r
+ }\r
+ \r
+ public void initialize() {\r
+ setIconImage(new ImageIcon(getClass().getResource("/gitblt-favicon.png")).getImage());\r
+ setTitle("Gitblit PKI Authority v" + Constants.VERSION + " (" + Constants.VERSION_DATE + ")");\r
+ setContentPane(getUI());\r
+ setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);\r
+ addWindowListener(new WindowAdapter() {\r
+ @Override\r
+ public void windowClosing(WindowEvent event) {\r
+ saveSizeAndPosition();\r
+ }\r
+\r
+ @Override\r
+ public void windowOpened(WindowEvent event) {\r
+ }\r
+ }); \r
+\r
+ setSizeAndPosition();\r
+ \r
+ File folder = new File(System.getProperty("user.dir"));\r
+ load(folder);\r
+ }\r
+ \r
+ private void setSizeAndPosition() {\r
+ String sz = null;\r
+ String pos = null;\r
+ try {\r
+ StoredConfig config = getConfig();\r
+ sz = config.getString("ui", null, "size");\r
+ pos = config.getString("ui", null, "position");\r
+ defaultDuration = config.getInt("new", "duration", 365);\r
+ } catch (Throwable t) {\r
+ t.printStackTrace();\r
+ }\r
+\r
+ // try to restore saved window size\r
+ if (StringUtils.isEmpty(sz)) {\r
+ setSize(850, 500);\r
+ } else {\r
+ String[] chunks = sz.split("x");\r
+ int width = Integer.parseInt(chunks[0]);\r
+ int height = Integer.parseInt(chunks[1]);\r
+ setSize(width, height);\r
+ }\r
+\r
+ // try to restore saved window position\r
+ if (StringUtils.isEmpty(pos)) {\r
+ setLocationRelativeTo(null);\r
+ } else {\r
+ String[] chunks = pos.split(",");\r
+ int x = Integer.parseInt(chunks[0]);\r
+ int y = Integer.parseInt(chunks[1]);\r
+ setLocation(x, y);\r
+ }\r
+ }\r
+\r
+ private void saveSizeAndPosition() {\r
+ try {\r
+ // save window size and position\r
+ StoredConfig config = getConfig();\r
+ Dimension sz = GitblitAuthority.this.getSize();\r
+ config.setString("ui", null, "size",\r
+ MessageFormat.format("{0,number,0}x{1,number,0}", sz.width, sz.height));\r
+ Point pos = GitblitAuthority.this.getLocationOnScreen();\r
+ config.setString("ui", null, "position",\r
+ MessageFormat.format("{0,number,0},{1,number,0}", pos.x, pos.y));\r
+ config.save();\r
+ } catch (Throwable t) {\r
+ Utils.showException(GitblitAuthority.this, t);\r
+ }\r
+ }\r
+ \r
+ private StoredConfig getConfig() throws IOException, ConfigInvalidException {\r
+ File configFile = new File(System.getProperty("user.dir"), X509Utils.CA_CONFIG);\r
+ FileBasedConfig config = new FileBasedConfig(configFile, FS.detect());\r
+ config.load();\r
+ return config;\r
+ }\r
+ \r
+ private IUserService loadUsers(File folder) {\r
+ File file = new File(folder, "gitblit.properties");\r
+ if (!file.exists()) {\r
+ return null;\r
+ }\r
+ gitblitSettings = new FileSettings(file.getAbsolutePath());\r
+ caKeystorePassword = gitblitSettings.getString(Keys.server.storePassword, null);\r
+ String us = gitblitSettings.getString(Keys.realm.userService, "users.conf");\r
+ String ext = us.substring(us.lastIndexOf(".") + 1).toLowerCase();\r
+ IUserService service = null;\r
+ if (!ext.equals("conf") && !ext.equals("properties")) {\r
+ if (us.equals("com.gitblit.LdapUserService")) {\r
+ us = gitblitSettings.getString(Keys.realm.ldap.backingUserService, "users.conf"); \r
+ } else if (us.equals("com.gitblit.LdapUserService")) {\r
+ us = gitblitSettings.getString(Keys.realm.redmine.backingUserService, "users.conf");\r
+ }\r
+ }\r
+\r
+ if (us.endsWith(".conf")) {\r
+ service = new ConfigUserService(new File(us));\r
+ } else {\r
+ throw new RuntimeException("Unsupported user service: " + us);\r
+ }\r
+ \r
+ service = new ConfigUserService(new File(us));\r
+ return service;\r
+ }\r
+ \r
+ private void load(File folder) {\r
+ this.folder = folder;\r
+ this.userService = loadUsers(folder);\r
+ if (userService != null) {\r
+ // build empty certificate model for all users\r
+ Map<String, UserCertificateModel> map = new HashMap<String, UserCertificateModel>();\r
+ for (String user : userService.getAllUsernames()) {\r
+ UserModel model = userService.getUserModel(user);\r
+ UserCertificateModel ucm = new UserCertificateModel(model); \r
+ map.put(user, ucm);\r
+ }\r
+ File certificatesConfigFile = new File(folder, X509Utils.CA_CONFIG);\r
+ FileBasedConfig config = new FileBasedConfig(certificatesConfigFile, FS.detect());\r
+ if (certificatesConfigFile.exists()) {\r
+ try {\r
+ config.load();\r
+ // replace user certificate model with actual data\r
+ List<UserCertificateModel> list = UserCertificateConfig.KEY.parse(config).list; \r
+ for (UserCertificateModel ucm : list) { \r
+ ucm.user = userService.getUserModel(ucm.user.username);\r
+ map.put(ucm.user.username, ucm);\r
+ }\r
+ } catch (IOException e) {\r
+ e.printStackTrace();\r
+ } catch (ConfigInvalidException e) {\r
+ e.printStackTrace();\r
+ }\r
+ }\r
+ \r
+ tableModel.list = new ArrayList<UserCertificateModel>(map.values());\r
+ Collections.sort(tableModel.list);\r
+ tableModel.fireTableDataChanged();\r
+ }\r
+ }\r
+ \r
+ private List<X509Certificate> findCerts(File folder, String username) {\r
+ List<X509Certificate> list = new ArrayList<X509Certificate>();\r
+ File userFolder = new File(folder, X509Utils.CERTS + File.separator + username);\r
+ if (!userFolder.exists()) {\r
+ return list;\r
+ }\r
+ File [] certs = userFolder.listFiles(new FilenameFilter() {\r
+ @Override\r
+ public boolean accept(File dir, String name) {\r
+ return name.toLowerCase().endsWith(".cer") || name.toLowerCase().endsWith(".crt");\r
+ }\r
+ });\r
+ try {\r
+ CertificateFactory factory = CertificateFactory.getInstance("X.509");\r
+ for (File cert : certs) { \r
+ BufferedInputStream is = new BufferedInputStream(new FileInputStream(cert));\r
+ X509Certificate x509 = (X509Certificate) factory.generateCertificate(is);\r
+ is.close();\r
+ list.add(x509);\r
+ }\r
+ } catch (Exception e) {\r
+ Utils.showException(GitblitAuthority.this, e);\r
+ }\r
+ return list;\r
+ }\r
+ \r
+ private Container getUI() { \r
+ userCertificatePanel = new UserCertificatePanel(this) {\r
+ \r
+ private static final long serialVersionUID = 1L;\r
+ @Override\r
+ public Insets getInsets() {\r
+ return Utils.INSETS;\r
+ }\r
+\r
+ @Override\r
+ public Date getDefaultExpiration() {\r
+ Calendar c = Calendar.getInstance();\r
+ c.add(Calendar.DATE, defaultDuration);\r
+ c.set(Calendar.HOUR_OF_DAY, 0);\r
+ c.set(Calendar.MINUTE, 0);\r
+ c.set(Calendar.SECOND, 0);\r
+ c.set(Calendar.MILLISECOND, 0);\r
+ return c.getTime();\r
+ }\r
+ \r
+ @Override\r
+ public void saveUser(String username, UserCertificateModel ucm) {\r
+ userService.updateUserModel(username, ucm.user);\r
+ }\r
+ \r
+ @Override\r
+ public void newCertificate(UserCertificateModel ucm, X509Metadata metadata, boolean sendEmail) {\r
+ Date notAfter = metadata.notAfter;\r
+ metadata.serverHostname = gitblitSettings.getString(Keys.web.siteName, "localhost");\r
+ UserModel user = ucm.user; \r
+ \r
+ // set default values from config file\r
+ File certificatesConfigFile = new File(folder, X509Utils.CA_CONFIG);\r
+ FileBasedConfig config = new FileBasedConfig(certificatesConfigFile, FS.detect());\r
+ if (certificatesConfigFile.exists()) {\r
+ try {\r
+ config.load();\r
+ } catch (Exception e) {\r
+ Utils.showException(GitblitAuthority.this, e);\r
+ }\r
+ NewCertificateConfig certificateConfig = NewCertificateConfig.KEY.parse(config);\r
+ certificateConfig.update(metadata);\r
+ }\r
+ \r
+ // restore expiration date\r
+ metadata.notAfter = notAfter;\r
+ \r
+ // set user's specified OID values\r
+ if (!StringUtils.isEmpty(user.organizationalUnit)) {\r
+ metadata.oids.put("OU", user.organizationalUnit);\r
+ }\r
+ if (!StringUtils.isEmpty(user.organization)) {\r
+ metadata.oids.put("O", user.organization);\r
+ }\r
+ if (!StringUtils.isEmpty(user.locality)) {\r
+ metadata.oids.put("L", user.locality);\r
+ }\r
+ if (!StringUtils.isEmpty(user.stateProvince)) {\r
+ metadata.oids.put("ST", user.stateProvince);\r
+ }\r
+ if (!StringUtils.isEmpty(user.countryCode)) {\r
+ metadata.oids.put("C", user.countryCode);\r
+ }\r
+\r
+ File caKeystoreFile = new File(folder, X509Utils.CA_KEY_STORE);\r
+ File zip = X509Utils.newClientBundle(metadata, caKeystoreFile, caKeystorePassword);\r
+ \r
+ // save latest expiration date\r
+ if (ucm.expires == null || metadata.notAfter.after(ucm.expires)) {\r
+ ucm.expires = metadata.notAfter;\r
+ }\r
+ ucm.update(config);\r
+ try {\r
+ config.save();\r
+ } catch (Exception e) {\r
+ Utils.showException(GitblitAuthority.this, e);\r
+ }\r
+ \r
+ // refresh user\r
+ ucm.certs = null;\r
+ int modelIndex = table.convertRowIndexToModel(table.getSelectedRow());\r
+ tableModel.fireTableDataChanged();\r
+ table.getSelectionModel().setSelectionInterval(modelIndex, modelIndex);\r
+ \r
+ if (sendEmail) {\r
+ // send email\r
+ try {\r
+ MailExecutor mail = new MailExecutor(gitblitSettings);\r
+ if (mail.isReady()) {\r
+ Message message = mail.createMessage(user.emailAddress);\r
+ message.setSubject("Your Gitblit client certificate for " + metadata.serverHostname);\r
+\r
+ // body of email\r
+ String body = X509Utils.processTemplate(new File(caKeystoreFile.getParentFile(), "mail.tmpl"), metadata);\r
+ if (StringUtils.isEmpty(body)) {\r
+ body = MessageFormat.format("Hi {0}\n\nHere is your client certificate bundle.\nInside the zip file are installation instructions.", user.getDisplayName());\r
+ }\r
+ Multipart mp = new MimeMultipart();\r
+ MimeBodyPart messagePart = new MimeBodyPart();\r
+ messagePart.setText(body);\r
+ mp.addBodyPart(messagePart);\r
+\r
+ // attach zip\r
+ MimeBodyPart filePart = new MimeBodyPart();\r
+ FileDataSource fds = new FileDataSource(zip);\r
+ filePart.setDataHandler(new DataHandler(fds));\r
+ filePart.setFileName(fds.getName());\r
+ mp.addBodyPart(filePart);\r
+\r
+ message.setContent(mp);\r
+\r
+ mail.sendNow(message);\r
+ } else {\r
+ JOptionPane.showMessageDialog(GitblitAuthority.this, "Sorry, the mail server settings are not configured properly.\nCan not send email.", Translation.get("gb.error"), JOptionPane.ERROR_MESSAGE);\r
+ }\r
+ } catch (Exception e) {\r
+ Utils.showException(GitblitAuthority.this, e);\r
+ }\r
+ }\r
+ }\r
+ \r
+ @Override\r
+ public void revoke(UserCertificateModel ucm, X509Certificate cert, RevocationReason reason) {\r
+ File caRevocationList = new File(folder, X509Utils.CA_REVOCATION_LIST);\r
+ File caKeystoreFile = new File(folder, X509Utils.CA_KEY_STORE);\r
+ if (X509Utils.revoke(cert, reason, caRevocationList, caKeystoreFile, caKeystorePassword)) {\r
+ File certificatesConfigFile = new File(folder, X509Utils.CA_CONFIG);\r
+ FileBasedConfig config = new FileBasedConfig(certificatesConfigFile, FS.detect());\r
+ if (certificatesConfigFile.exists()) {\r
+ try {\r
+ config.load();\r
+ } catch (Exception e) {\r
+ Utils.showException(GitblitAuthority.this, e);\r
+ }\r
+ }\r
+ // add serial to revoked list\r
+ ucm.revoke(cert.getSerialNumber(), reason);\r
+ ucm.update(config);\r
+ try {\r
+ config.save();\r
+ } catch (Exception e) {\r
+ Utils.showException(GitblitAuthority.this, e);\r
+ }\r
+ \r
+ // refresh user\r
+ ucm.certs = null;\r
+ int modelIndex = table.convertRowIndexToModel(table.getSelectedRow());\r
+ tableModel.fireTableDataChanged();\r
+ table.getSelectionModel().setSelectionInterval(modelIndex, modelIndex);\r
+ }\r
+ }\r
+ };\r
+ \r
+ table = Utils.newTable(tableModel, Utils.DATE_FORMAT);\r
+ table.setRowSorter(defaultSorter);\r
+ table.setDefaultRenderer(CertificateStatus.class, new CertificateStatusRenderer());\r
+ table.getSelectionModel().addListSelectionListener(new ListSelectionListener() {\r
+\r
+ @Override\r
+ public void valueChanged(ListSelectionEvent e) {\r
+ if (e.getValueIsAdjusting()) {\r
+ return;\r
+ }\r
+ int row = table.getSelectedRow();\r
+ if (row < 0) {\r
+ return;\r
+ }\r
+ int modelIndex = table.convertRowIndexToModel(row);\r
+ UserCertificateModel ucm = tableModel.get(modelIndex);\r
+ if (ucm.certs == null) {\r
+ ucm.certs = findCerts(folder, ucm.user.username);\r
+ }\r
+ userCertificatePanel.setUserCertificateModel(ucm);\r
+ }\r
+ });\r
+ \r
+ JPanel usersPanel = new JPanel(new BorderLayout()) {\r
+ \r
+ private static final long serialVersionUID = 1L;\r
+\r
+ @Override\r
+ public Insets getInsets() {\r
+ return Utils.INSETS;\r
+ }\r
+ };\r
+ usersPanel.add(new HeaderPanel(Translation.get("gb.users"), "users_16x16.png"), BorderLayout.NORTH);\r
+ usersPanel.add(new JScrollPane(table), BorderLayout.CENTER);\r
+ usersPanel.setMinimumSize(new Dimension(400, 10));\r
+ \r
+ final JTextField filterTextfield = new JTextField(20);\r
+ filterTextfield.addActionListener(new ActionListener() {\r
+ public void actionPerformed(ActionEvent e) {\r
+ filterUsers(filterTextfield.getText());\r
+ }\r
+ });\r
+ filterTextfield.addKeyListener(new KeyAdapter() {\r
+ public void keyReleased(KeyEvent e) {\r
+ filterUsers(filterTextfield.getText());\r
+ }\r
+ });\r
+\r
+ JPanel userControls = new JPanel(new FlowLayout(FlowLayout.RIGHT, 5, 5));\r
+ userControls.add(new JLabel(Translation.get("gb.filter")));\r
+ userControls.add(filterTextfield);\r
+ \r
+ JPanel leftPanel = new JPanel(new BorderLayout());\r
+ leftPanel.add(userControls, BorderLayout.NORTH);\r
+ leftPanel.add(usersPanel, BorderLayout.CENTER);\r
+ \r
+ userCertificatePanel.setMinimumSize(new Dimension(375, 10));\r
+ \r
+ JPanel root = new JPanel(new BorderLayout()) {\r
+ private static final long serialVersionUID = 1L;\r
+ public Insets getInsets() {\r
+ return Utils.INSETS;\r
+ }\r
+ };\r
+ JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftPanel, userCertificatePanel);\r
+ splitPane.setDividerLocation(1d);\r
+ root.add(splitPane);\r
+ return root;\r
+ }\r
+ \r
+ private void filterUsers(final String fragment) {\r
+ if (StringUtils.isEmpty(fragment)) {\r
+ table.setRowSorter(defaultSorter);\r
+ return;\r
+ }\r
+ RowFilter<UserCertificateTableModel, Object> containsFilter = new RowFilter<UserCertificateTableModel, Object>() {\r
+ public boolean include(Entry<? extends UserCertificateTableModel, ? extends Object> entry) {\r
+ for (int i = entry.getValueCount() - 1; i >= 0; i--) {\r
+ if (entry.getStringValue(i).toLowerCase().contains(fragment.toLowerCase())) {\r
+ return true;\r
+ }\r
+ }\r
+ return false;\r
+ }\r
+ };\r
+ TableRowSorter<UserCertificateTableModel> sorter = new TableRowSorter<UserCertificateTableModel>(\r
+ tableModel);\r
+ sorter.setRowFilter(containsFilter);\r
+ table.setRowSorter(sorter);\r
+ }\r
+}\r