Add license management

This commit is contained in:
robin shen 2017-11-09 20:46:24 +08:00
parent 74dcf0fe7c
commit d55805ba4e
15 changed files with 413 additions and 6 deletions

View File

@ -5,6 +5,7 @@ import java.io.IOException;
import java.net.InetAddress;
import java.util.List;
import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.servlet.ServletException;
@ -17,6 +18,7 @@ import org.apache.shiro.SecurityUtils;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import com.gitplex.server.manager.ConfigManager;
import com.gitplex.server.manager.ProjectManager;
import com.gitplex.server.manager.UserManager;
import com.gitplex.server.model.Project;
@ -26,9 +28,11 @@ import com.gitplex.server.model.User;
import com.gitplex.server.model.support.BranchProtection;
import com.gitplex.server.model.support.TagProtection;
import com.gitplex.server.persistence.annotation.Sessional;
import com.gitplex.server.persistence.dao.EntityCriteria;
import com.gitplex.server.security.ProjectPrivilege;
import com.gitplex.server.security.permission.ProjectPermission;
import com.gitplex.utils.StringUtils;
import com.gitplex.utils.license.LicenseDetail;
import com.google.common.base.Preconditions;
@SuppressWarnings("serial")
@ -41,18 +45,24 @@ public class GitPreReceiveCallback extends HttpServlet {
private final UserManager userManager;
private final ConfigManager configManager;
@Inject
public GitPreReceiveCallback(ProjectManager projectManager, UserManager userManager) {
public GitPreReceiveCallback(ProjectManager projectManager, UserManager userManager, ConfigManager configManager) {
this.projectManager = projectManager;
this.userManager = userManager;
this.configManager = configManager;
}
private void error(Output output, String refName, String... messages) {
private void error(Output output, @Nullable String refName, String... messages) {
output.markError();
output.writeLine();
output.writeLine("*******************************************************");
output.writeLine("*");
if (refName != null)
output.writeLine("* ERROR PUSHING REF: " + refName);
else
output.writeLine("* ERROR PUSHING");
output.writeLine("-------------------------------------------------------");
for (String message: messages)
output.writeLine("* " + message);
@ -64,6 +74,18 @@ public class GitPreReceiveCallback extends HttpServlet {
@Sessional
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
int userCount = userManager.count(EntityCriteria.of(User.class));
int licenseLimit = LicenseDetail.FREE_LICENSE_USERS;
LicenseDetail license = configManager.getLicense();
if (license != null && license.getRemainingDays() >= 0) {
licenseLimit += license.getLicensedUsers();
}
if (userCount > licenseLimit) {
String message = String.format("Push is disabled as number of users in system exceeds license limit");
response.sendError(HttpServletResponse.SC_FORBIDDEN, message);
return;
}
String clientIp = request.getHeader("X-Forwarded-For");
if (clientIp == null) clientIp = request.getRemoteAddr();

View File

@ -9,6 +9,7 @@ import com.gitplex.server.model.support.setting.SecuritySetting;
import com.gitplex.server.model.support.setting.SystemSetting;
import com.gitplex.server.persistence.dao.EntityManager;
import com.gitplex.server.security.authenticator.Authenticator;
import com.gitplex.utils.license.LicenseDetail;
public interface ConfigManager extends EntityManager<Config> {
@ -93,4 +94,9 @@ public interface ConfigManager extends EntityManager<Config> {
void saveAuthenticator(@Nullable Authenticator authenticator);
@Nullable
LicenseDetail getLicense();
void saveLicense(@Nullable LicenseDetail license);
}

View File

@ -20,6 +20,7 @@ import com.gitplex.server.persistence.dao.AbstractEntityManager;
import com.gitplex.server.persistence.dao.Dao;
import com.gitplex.server.persistence.dao.EntityCriteria;
import com.gitplex.server.security.authenticator.Authenticator;
import com.gitplex.utils.license.LicenseDetail;
import com.google.common.base.Preconditions;
@Singleton
@ -37,6 +38,8 @@ public class DefaultConfigManager extends AbstractEntityManager<Config> implemen
private volatile Long authenticatorConfigId;
private volatile Long licenseConfigId;
@Inject
public DefaultConfigManager(Dao dao, DataManager dataManager) {
super(dao);
@ -191,4 +194,30 @@ public class DefaultConfigManager extends AbstractEntityManager<Config> implemen
dao.persist(config);
}
@Sessional
@Override
public LicenseDetail getLicense() {
Config config;
if (licenseConfigId == null) {
config = getConfig(Key.LICENSE);
Preconditions.checkNotNull(config);
licenseConfigId = config.getId();
} else {
config = load(licenseConfigId);
}
return (LicenseDetail) config.getSetting();
}
@Transactional
@Override
public void saveLicense(LicenseDetail license) {
Config config = getConfig(Key.LICENSE);
if (config == null) {
config = new Config();
config.setKey(Key.LICENSE);
}
config.setSetting(license);
dao.persist(config);
}
}

View File

@ -186,6 +186,11 @@ public class DefaultDataManager implements DataManager, Serializable {
});
}
Config licenseKeyConfig = configManager.getConfig(Key.LICENSE);
if (licenseKeyConfig == null) {
configManager.saveLicense(null);
}
return manualConfigs;
}

View File

@ -387,4 +387,22 @@ public class DatabaseMigrator {
}
}
private void migrate11(File dataDir, Stack<Integer> versions) {
for (File file: dataDir.listFiles()) {
if (file.getName().startsWith("Configs.xml")) {
VersionedDocument dom = VersionedDocument.fromFile(file);
long maxId = 0;
for (Element element: dom.getRootElement().elements()) {
Long id = Long.parseLong(element.elementTextTrim("id"));
if (maxId < id)
maxId = id;
}
Element licenseConfigElement = dom.getRootElement().addElement("com.gitplex.server.model.Config");
licenseConfigElement.addElement("id").setText(String.valueOf(maxId+1));
licenseConfigElement.addElement("key").setText("LICENSE");
dom.writeToFile(file, false);
}
}
}
}

View File

@ -23,7 +23,7 @@ public class Config extends AbstractEntity {
private static final long serialVersionUID = 1L;
public enum Key {SYSTEM, MAIL, BACKUP, SECURITY, AUTHENTICATOR};
public enum Key {SYSTEM, MAIL, BACKUP, SECURITY, AUTHENTICATOR, LICENSE};
/*
* Optimistic lock is necessary to ensure database integrity when update

View File

@ -53,7 +53,7 @@
<dependency>
<groupId>org.webjars</groupId>
<artifactId>font-awesome</artifactId>
<version>4.5.0</version>
<version>4.7.0</version>
<exclusions>
<exclusion>
<groupId>org.webjars</groupId>

View File

@ -11,6 +11,7 @@ import org.apache.wicket.request.mapper.CompoundRequestMapper;
import com.gitplex.server.web.page.admin.authenticator.AuthenticatorPage;
import com.gitplex.server.web.page.admin.databasebackup.DatabaseBackupPage;
import com.gitplex.server.web.page.admin.licensemanagement.LicenseManagementPage;
import com.gitplex.server.web.page.admin.mailsetting.MailSettingPage;
import com.gitplex.server.web.page.admin.securitysetting.SecuritySettingPage;
import com.gitplex.server.web.page.admin.serverinformation.ServerInformationPage;
@ -103,6 +104,7 @@ public class GitPlexUrlMapper extends CompoundRequestMapper {
add(new GitPlexPageMapper("administration/settings/authenticator", AuthenticatorPage.class));
add(new GitPlexPageMapper("administration/server-log", ServerLogPage.class));
add(new GitPlexPageMapper("administration/server-information", ServerInformationPage.class));
add(new GitPlexPageMapper("administration/license-management", LicenseManagementPage.class));
}
private void addUserPages() {

View File

@ -16,6 +16,7 @@ import com.gitplex.server.web.component.tabbable.PageTab;
import com.gitplex.server.web.component.tabbable.Tabbable;
import com.gitplex.server.web.page.admin.authenticator.AuthenticatorPage;
import com.gitplex.server.web.page.admin.databasebackup.DatabaseBackupPage;
import com.gitplex.server.web.page.admin.licensemanagement.LicenseManagementPage;
import com.gitplex.server.web.page.admin.mailsetting.MailSettingPage;
import com.gitplex.server.web.page.admin.securitysetting.SecuritySettingPage;
import com.gitplex.server.web.page.admin.serverinformation.ServerInformationPage;
@ -43,6 +44,7 @@ public abstract class AdministrationPage extends LayoutPage {
tabs.add(new AdministrationTab("Database Backup", "fa fa-fw fa-database", DatabaseBackupPage.class));
tabs.add(new AdministrationTab("Server Log", "fa fa-fw fa-file-text-o", ServerLogPage.class));
tabs.add(new AdministrationTab("Server Information", "fa fa-fw fa-desktop", ServerInformationPage.class));
tabs.add(new AdministrationTab("License Management", "fa fa-fw fa-vcard-o", LicenseManagementPage.class));
add(new Tabbable("tabs", tabs));
}

View File

@ -27,3 +27,15 @@
color: white;
}
.license-detail .properties td {
padding: 0 16px 16px 0;
}
.license-detail .properties tr:last-child td {
padding-bottom: 0;
}
.license-detail .properties td.name {
width: 200px;
}
.license-management .actions a {
margin-right: 16px;
}

View File

@ -0,0 +1,52 @@
<wicket:extend>
<div class="license-management panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">Current License</h4>
</div>
<div class="panel-body">
<div wicket:id="licenseDetail"></div>
</div>
</div>
<div class="actions">
<a wicket:id="inputLicenseKey" class="btn btn-primary">Install New License</a>
<a wicket:id="removeLicense" class="btn btn-danger">Remove Current License</a>
</div>
<wicket:fragment wicket:id="licenseDetailFrag">
<div class="license-detail">
<div wicket:id="expiredNotice" class="expired alert alert-danger"></div>
<div wicket:id="aboutToExpireNotice" class="about-to-expire alert alert-warning"></div>
<table class="properties">
<tr>
<td class="name">Users</td>
<td wicket:id="users" class="value"></td>
</tr>
<tr>
<td class="name">Issue Date</td>
<td wicket:id="issueDate" class="value"></td>
</tr>
<tr>
<td class="name">Expiration Date</td>
<td wicket:id="expirationDate" class="value"></td>
</tr>
</table>
</div>
</wicket:fragment>
<wicket:fragment wicket:id="licenseKeyInputFrag">
<form wicket:id="form" class="credit-key-input">
<div class="modal-header">
<button wicket:id="close" type="button" class="close">&times;</button>
<h4 class="modal-title">Input License Key</h4>
</div>
<div class="modal-body">
<div wicket:id="feedback"></div>
<textarea wicket:id="licenseKey" rows="10" class="form-control autofocus"></textarea>
<p class="help-block">License key can be generated from account at
<a href="https://www.gitplex.com">gitplex.com</a>
</div>
<div class="modal-footer">
<button wicket:id="ok" class="btn btn-primary">Ok</button>
<button wicket:id="cancel" class="btn btn-default">Cancel</button>
</div>
</form>
</wicket:fragment>
</wicket:extend>

View File

@ -0,0 +1,226 @@
package com.gitplex.server.web.page.admin.licensemanagement;
import javax.annotation.Nullable;
import org.apache.wicket.Component;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.markup.html.AjaxLink;
import org.apache.wicket.ajax.markup.html.form.AjaxButton;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.TextArea;
import org.apache.wicket.markup.html.link.Link;
import org.apache.wicket.markup.html.panel.Fragment;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.LoadableDetachableModel;
import org.joda.time.DateTime;
import com.gitplex.server.GitPlex;
import com.gitplex.server.manager.ConfigManager;
import com.gitplex.server.manager.UserManager;
import com.gitplex.server.model.User;
import com.gitplex.server.persistence.dao.EntityCriteria;
import com.gitplex.server.web.WebConstants;
import com.gitplex.server.web.component.modal.ModalLink;
import com.gitplex.server.web.component.modal.ModalPanel;
import com.gitplex.server.web.page.admin.AdministrationPage;
import com.gitplex.server.web.util.ConfirmOnClick;
import com.gitplex.utils.license.LicenseDetail;
import de.agilecoders.wicket.core.markup.html.bootstrap.common.NotificationPanel;
@SuppressWarnings("serial")
public class LicenseManagementPage extends AdministrationPage {
private static final String LICENSE_DETAIL_ID = "licenseDetail";
private String licenseKey;
private final IModel<LicenseDetail> licenseModel = new LoadableDetachableModel<LicenseDetail>() {
@Override
protected LicenseDetail load() {
return GitPlex.getInstance(ConfigManager.class).getLicense();
}
};
private final IModel<Integer> userCountModel = new LoadableDetachableModel<Integer>() {
@Override
protected Integer load() {
return GitPlex.getInstance(UserManager.class).count(EntityCriteria.of(User.class));
}
};
@Override
protected void onInitialize() {
super.onInitialize();
LicenseDetail licenseDetail = getLicense();
if (licenseDetail != null) {
Fragment fragment = new Fragment(LICENSE_DETAIL_ID, "licenseDetailFrag", this);
fragment.add(new Label("expiredNotice", new LoadableDetachableModel<String>() {
@Override
protected String load() {
return String.format("License was expired. The free %d-user license is now taking effect",
LicenseDetail.FREE_LICENSE_USERS);
}
}).setVisible(licenseDetail.getRemainingDays()<0).setEscapeModelStrings(false));
fragment.add(new Label("aboutToExpireNotice", new LoadableDetachableModel<String>() {
@Override
protected String load() {
if (getUserCount() > LicenseDetail.FREE_LICENSE_USERS) {
return String.format(""
+ "License will expire in %d days. The free %d-user license will take effect "
+ "after expiration", licenseDetail.getRemainingDays(), LicenseDetail.FREE_LICENSE_USERS);
} else {
return String.format("License will expire in %d days, The free %d-user license will take "
+ "effect after expiration", licenseDetail.getRemainingDays(),
LicenseDetail.FREE_LICENSE_USERS);
}
}
}).setVisible(licenseDetail.getRemainingDays()>=0 && licenseDetail.getRemainingDays() < LicenseDetail.ABOUT_TO_EXPIRE_DAYS).setEscapeModelStrings(false));
String usersInfo = String.format("%d free + %d licensed (<a href=\"https://www.gitplex.com/purchase\">add more</a>)",
LicenseDetail.FREE_LICENSE_USERS, licenseDetail.getLicensedUsers());
fragment.add(new Label("users", usersInfo).setEscapeModelStrings(false));
fragment.add(new Label("issueDate",
WebConstants.DATE_FORMATTER.print(new DateTime(licenseDetail.getIssueDate()))));
fragment.add(new Label("expirationDate",
WebConstants.DATE_FORMATTER.print(new DateTime(licenseDetail.getExpirationDate())) + " (<a href=\"https://www.gitplex.com/purchase\">renew</a>)").setEscapeModelStrings(false));
add(fragment);
} else {
if (getUserCount() > LicenseDetail.FREE_LICENSE_USERS) {
String message = String.format(""
+ "Free %d-user license. <a href=\"https://www.gitplex.com/purchase\">Add additional users</a>",
LicenseDetail.FREE_LICENSE_USERS);
add(new Label(LICENSE_DETAIL_ID, message).setEscapeModelStrings(false));
} else {
String message = String.format(""
+ "Free %d-user license. <a href=\"https://www.gitplex.com/purchase\">Add additional users</a>",
LicenseDetail.FREE_LICENSE_USERS);
add(new Label(LICENSE_DETAIL_ID, message).setEscapeModelStrings(false));
}
}
add(new ModalLink("inputLicenseKey") {
@Override
protected Component newContent(String id, ModalPanel modal) {
Fragment fragment = new Fragment(id, "licenseKeyInputFrag", LicenseManagementPage.this);
licenseKey = null;
Form<?> form = new Form<Void>("form");
form.add(new TextArea<String>("licenseKey", new IModel<String>() {
@Override
public void detach() {
}
@Override
public String getObject() {
return licenseKey;
}
@Override
public void setObject(String object) {
licenseKey = object;
}
}));
NotificationPanel feedback = new NotificationPanel("feedback", fragment);
feedback.setOutputMarkupId(true);
form.add(feedback);
form.add(new AjaxButton("ok") {
@Override
protected void onSubmit(AjaxRequestTarget target, Form<?> form) {
super.onSubmit(target, form);
if (licenseKey == null) {
error("Please input license key");
target.add(feedback);
} else {
LicenseDetail license = LicenseDetail.decode(licenseKey);
if (license == null) {
error("Invalid license key");
target.add(feedback);
} else {
GitPlex.getInstance(ConfigManager.class).saveLicense(license);
setResponsePage(LicenseManagementPage.class);
getSession().success("License key applied successfully");
}
}
}
});
form.add(new AjaxLink<Void>("cancel") {
@Override
public void onClick(AjaxRequestTarget target) {
modal.close();
}
});
form.add(new AjaxLink<Void>("close") {
@Override
public void onClick(AjaxRequestTarget target) {
modal.close();
}
});
fragment.add(form);
return fragment;
}
});
String confirmMessage = String.format(""
+ "The free-%d user license will take effect after removal. Do you really want to continue?",
LicenseDetail.FREE_LICENSE_USERS);
add(new Link<Void>("removeLicense") {
@Override
public void onClick() {
GitPlex.getInstance(ConfigManager.class).saveLicense(null);
setResponsePage(LicenseManagementPage.class);
}
@Override
protected void onConfigure() {
super.onConfigure();
setVisible(getLicense() != null);
}
}.add(new ConfirmOnClick(confirmMessage)));
}
@Nullable
private LicenseDetail getLicense() {
return licenseModel.getObject();
}
private int getUserCount() {
return userCountModel.getObject();
}
@Override
protected void onDetach() {
userCountModel.detach();
licenseModel.detach();
super.onDetach();
}
}

View File

@ -26,6 +26,11 @@
<div class="help pull-right">
<a wicket:id="docLink" title="Help"><i class="fa fa-question"></i></a>
</div>
<wicket:enclosure child="pushDisabled">
<div class="push-disabled pull-right">
<span wicket:id="pushDisabled" class="label label-danger">Git Push Disabled</span>
</div>
</wicket:enclosure>
</div>
<div class="body">
<wicket:child></wicket:child>

View File

@ -25,9 +25,12 @@ import com.gitplex.launcher.loader.AppLoader;
import com.gitplex.launcher.loader.Plugin;
import com.gitplex.server.GitPlex;
import com.gitplex.server.manager.ConfigManager;
import com.gitplex.server.manager.UserManager;
import com.gitplex.server.model.User;
import com.gitplex.server.persistence.dao.EntityCriteria;
import com.gitplex.server.security.SecurityUtils;
import com.gitplex.server.web.ComponentRenderer;
import com.gitplex.server.web.behavior.TooltipBehavior;
import com.gitplex.server.web.component.avatar.AvatarLink;
import com.gitplex.server.web.component.floating.AlignPlacement;
import com.gitplex.server.web.component.floating.FloatingPanel;
@ -43,6 +46,10 @@ import com.gitplex.server.web.page.user.UserProfilePage;
import com.gitplex.server.web.websocket.PageDataChanged;
import com.gitplex.server.web.websocket.TaskChangedRegion;
import com.gitplex.server.web.websocket.WebSocketRegion;
import com.gitplex.utils.license.LicenseDetail;
import de.agilecoders.wicket.core.markup.html.bootstrap.components.TooltipConfig;
import de.agilecoders.wicket.core.markup.html.bootstrap.components.TooltipConfig.Placement;
@SuppressWarnings("serial")
public abstract class LayoutPage extends BasePage {
@ -75,6 +82,21 @@ public abstract class LayoutPage extends BasePage {
});
int userCount = GitPlex.getInstance(UserManager.class).count(EntityCriteria.of(User.class));
int licenseLimit = LicenseDetail.FREE_LICENSE_USERS;
LicenseDetail license = GitPlex.getInstance(ConfigManager.class).getLicense();
if (license != null && license.getRemainingDays()>=0)
licenseLimit += license.getLicensedUsers();
if (userCount > licenseLimit) {
String tooltip = String.format(""
+ "Git push is disabled as number of users (%d) in system exceeds license limit (%d).",
userCount, licenseLimit);
TooltipBehavior tooltipBehavior = new TooltipBehavior(Model.of(tooltip),
new TooltipConfig().withPlacement(Placement.bottom));
head.add(new WebMarkupContainer("pushDisabled").add(tooltipBehavior));
} else {
head.add(new WebMarkupContainer("pushDisabled").setVisible(false));
}
head.add(new ExternalLink("docLink", GitPlex.getInstance().getDocLink()));
User user = getLoginUser();

View File

@ -13,6 +13,12 @@ html, body {
text-decoration: none;
color: #CCC;
}
#main>.head .push-disabled {
padding-top: 6px;
}
#main>.head .push-disabled .label {
font-size: 15px;
}
#main>.head>.logo .fa {
font-size: 28px;
color: #EEE;