feat: Able to support SSO provider without email address (OD-2545)

This commit is contained in:
Robin Shen 2025-09-02 09:02:08 +08:00
parent 9c1373442b
commit 78b31191cc
120 changed files with 1958 additions and 1142 deletions

View File

@ -275,7 +275,7 @@
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>oauth2-oidc-sdk</artifactId>
<version>11.9.1</version>
<version>11.28</version>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>

View File

@ -170,6 +170,8 @@ import io.onedev.server.entitymanager.ReviewedDiffManager;
import io.onedev.server.entitymanager.RoleManager;
import io.onedev.server.entitymanager.SettingManager;
import io.onedev.server.entitymanager.SshKeyManager;
import io.onedev.server.entitymanager.SsoAccountManager;
import io.onedev.server.entitymanager.SsoProviderManager;
import io.onedev.server.entitymanager.StopwatchManager;
import io.onedev.server.entitymanager.UserAuthorizationManager;
import io.onedev.server.entitymanager.UserInvitationManager;
@ -256,6 +258,8 @@ import io.onedev.server.entitymanager.impl.DefaultReviewedDiffManager;
import io.onedev.server.entitymanager.impl.DefaultRoleManager;
import io.onedev.server.entitymanager.impl.DefaultSettingManager;
import io.onedev.server.entitymanager.impl.DefaultSshKeyManager;
import io.onedev.server.entitymanager.impl.DefaultSsoAccountManager;
import io.onedev.server.entitymanager.impl.DefaultSsoProviderManager;
import io.onedev.server.entitymanager.impl.DefaultStopwatchManager;
import io.onedev.server.entitymanager.impl.DefaultUserAuthorizationManager;
import io.onedev.server.entitymanager.impl.DefaultUserInvitationManager;
@ -564,6 +568,8 @@ public class CoreModule extends AbstractPluginModule {
bind(PullRequestCommentRevisionManager.class).to(DefaultPullRequestCommentRevisionManager.class);
bind(IssueDescriptionRevisionManager.class).to(DefaultIssueDescriptionRevisionManager.class);
bind(PullRequestDescriptionRevisionManager.class).to(DefaultPullRequestDescriptionRevisionManager.class);
bind(SsoProviderManager.class).to(DefaultSsoProviderManager.class);
bind(SsoAccountManager.class).to(DefaultSsoAccountManager.class);
bind(BaseAuthorizationManager.class).to(DefaultBaseAuthorizationManager.class);
bind(WebHookManager.class);

View File

@ -803,10 +803,6 @@ public class DefaultDataManager implements DataManager, Serializable {
setting = settingManager.findSetting(Key.JOB_EXECUTORS);
if (setting == null)
settingManager.saveJobExecutors(new ArrayList<>());
setting = settingManager.findSetting(Key.SSO_CONNECTORS);
if (setting == null) {
settingManager.saveSsoConnectors(Lists.newArrayList());
}
setting = settingManager.findSetting(Key.GROOVY_SCRIPTS);
if (setting == null) {
settingManager.saveGroovyScripts(Lists.newArrayList());

View File

@ -8181,4 +8181,61 @@ public class DataMigrator {
}
}
private void migrate208(File dataDir, Stack<Integer> versions) {
VersionedXmlDoc ssoProvidersDom = new VersionedXmlDoc();
Element ssoProvidersListElement = ssoProvidersDom.addElement("list");
long ssoProviderId = 1L;
var groupIds = new HashMap<String, String>();
for (File file : dataDir.listFiles()) {
if (file.getName().startsWith("Groups.xml")) {
VersionedXmlDoc dom = VersionedXmlDoc.fromFile(file);
for (Element element : dom.getRootElement().elements()) {
groupIds.put(element.elementText("name").trim(), element.elementText("id").trim());
}
}
}
for (File file : dataDir.listFiles()) {
if (file.getName().startsWith("Settings.xml")) {
VersionedXmlDoc dom = VersionedXmlDoc.fromFile(file);
for (Element element : dom.getRootElement().elements()) {
String key = element.elementTextTrim("key");
if (key.equals("SSO_CONNECTORS")) {
Element valueElement = element.element("value");
if (valueElement != null) {
for (var connectorElement : valueElement.elements()) {
var connectorClass = connectorElement.getName();
var nameElement = connectorElement.element("name");
var defaultGroupElement = connectorElement.element("defaultGroup");
var ssoProviderElement = ssoProvidersListElement.addElement("io.onedev.server.model.SsoProvider");
ssoProviderElement.addAttribute("revision", "0.0");
ssoProviderElement.addElement("id").setText(String.valueOf(ssoProviderId++));
nameElement.detach();
ssoProviderElement.add(nameElement);
if (defaultGroupElement != null) {
var groupId = groupIds.get(defaultGroupElement.getText().trim());
if (groupId != null)
ssoProviderElement.addElement("defaultGroup").setText(groupId);
defaultGroupElement.detach();
}
connectorElement.setName("connector");
connectorElement.addAttribute("class", connectorClass);
connectorElement.detach();
ssoProviderElement.add(connectorElement);
}
}
element.detach();
}
}
dom.writeToFile(file, false);
}
}
if (ssoProviderId > 1) {
ssoProvidersDom.writeToFile(new File(dataDir, "SsoProviders.xml"), false);
}
}
}

View File

@ -1,13 +1,35 @@
package io.onedev.server.entitymanager;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
import io.onedev.server.model.Setting;
import io.onedev.server.model.Setting.Key;
import io.onedev.server.model.support.administration.*;
import io.onedev.server.model.support.administration.AgentSetting;
import io.onedev.server.model.support.administration.AlertSetting;
import io.onedev.server.model.support.administration.AuditSetting;
import io.onedev.server.model.support.administration.BackupSetting;
import io.onedev.server.model.support.administration.BrandingSetting;
import io.onedev.server.model.support.administration.ClusterSetting;
import io.onedev.server.model.support.administration.GlobalBuildSetting;
import io.onedev.server.model.support.administration.GlobalIssueSetting;
import io.onedev.server.model.support.administration.GlobalPackSetting;
import io.onedev.server.model.support.administration.GlobalProjectSetting;
import io.onedev.server.model.support.administration.GlobalPullRequestSetting;
import io.onedev.server.model.support.administration.GpgSetting;
import io.onedev.server.model.support.administration.GroovyScript;
import io.onedev.server.model.support.administration.PerformanceSetting;
import io.onedev.server.model.support.administration.SecuritySetting;
import io.onedev.server.model.support.administration.ServiceDeskSetting;
import io.onedev.server.model.support.administration.SshSetting;
import io.onedev.server.model.support.administration.SystemSetting;
import io.onedev.server.model.support.administration.authenticator.Authenticator;
import io.onedev.server.model.support.administration.emailtemplates.EmailTemplates;
import io.onedev.server.model.support.administration.jobexecutor.JobExecutor;
import io.onedev.server.model.support.administration.mailservice.MailService;
import io.onedev.server.model.support.administration.sso.SsoConnector;
import io.onedev.server.persistence.dao.EntityManager;
import io.onedev.server.util.usage.Usage;
import io.onedev.server.web.component.issue.workflowreconcile.UndefinedFieldResolution;
@ -15,11 +37,6 @@ import io.onedev.server.web.component.issue.workflowreconcile.UndefinedFieldValu
import io.onedev.server.web.component.issue.workflowreconcile.UndefinedFieldValuesResolution;
import io.onedev.server.web.page.layout.ContributedAdministrationSetting;
import javax.annotation.Nullable;
import java.util.Collection;
import java.util.List;
import java.util.Map;
public interface SettingManager extends EntityManager<Setting> {
@Nullable
@ -156,10 +173,6 @@ public interface SettingManager extends EntityManager<Setting> {
void saveSshSetting(SshSetting sshSetting);
void saveGpgSetting(GpgSetting gpgSetting);
List<SsoConnector> getSsoConnectors();
void saveSsoConnectors(List<SsoConnector> ssoConnectors);
Map<String, ContributedAdministrationSetting> getContributedSettings();

View File

@ -0,0 +1,16 @@
package io.onedev.server.entitymanager;
import javax.annotation.Nullable;
import io.onedev.server.model.SsoAccount;
import io.onedev.server.model.SsoProvider;
import io.onedev.server.persistence.dao.EntityManager;
public interface SsoAccountManager extends EntityManager<SsoAccount> {
void create(SsoAccount ssoAccount);
@Nullable
SsoAccount find(SsoProvider provider, String subject);
}

View File

@ -0,0 +1,15 @@
package io.onedev.server.entitymanager;
import javax.annotation.Nullable;
import io.onedev.server.model.SsoProvider;
import io.onedev.server.persistence.dao.EntityManager;
public interface SsoProviderManager extends EntityManager<SsoProvider> {
void createOrUpdate(SsoProvider ssoProvider);
@Nullable
SsoProvider find(String name);
}

View File

@ -125,6 +125,10 @@ public class DefaultGroupManager extends BaseEntityManager<Group> implements Gro
usage.add(settingManager.onDeleteGroup(group.getName()));
usage.checkInUse("Group '" + group.getName() + "'");
var query = getSession().createQuery("update SsoProvider set defaultGroup=null where defaultGroup=:group");
query.setParameter("group", group);
query.executeUpdate();
dao.remove(group);
}

View File

@ -53,7 +53,6 @@ import io.onedev.server.model.support.administration.authenticator.Authenticator
import io.onedev.server.model.support.administration.emailtemplates.EmailTemplates;
import io.onedev.server.model.support.administration.jobexecutor.JobExecutor;
import io.onedev.server.model.support.administration.mailservice.MailService;
import io.onedev.server.model.support.administration.sso.SsoConnector;
import io.onedev.server.persistence.annotation.Sessional;
import io.onedev.server.persistence.annotation.Transactional;
import io.onedev.server.persistence.dao.BaseEntityManager;
@ -246,12 +245,6 @@ public class DefaultSettingManager extends BaseEntityManager<Setting> implements
return (PerformanceSetting) getSettingValue(Key.PERFORMANCE);
}
@SuppressWarnings("unchecked")
@Override
public List<SsoConnector> getSsoConnectors() {
return (List<SsoConnector>) getSettingValue(Key.SSO_CONNECTORS);
}
@Override
public ServiceDeskSetting getServiceDeskSetting() {
return (ServiceDeskSetting) getSettingValue(Key.SERVICE_DESK_SETTING);
@ -406,12 +399,6 @@ public class DefaultSettingManager extends BaseEntityManager<Setting> implements
saveSetting(Key.GPG, gpgSetting);
}
@Transactional
@Override
public void saveSsoConnectors(List<SsoConnector> ssoConnectors) {
saveSetting(Key.SSO_CONNECTORS, (Serializable) ssoConnectors);
}
@Transactional
@Override
public void saveContributedSettings(Map<String, ContributedAdministrationSetting> contributedSettings) {

View File

@ -0,0 +1,40 @@
package io.onedev.server.entitymanager.impl;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.hibernate.criterion.Restrictions;
import io.onedev.server.entitymanager.SsoAccountManager;
import io.onedev.server.model.SsoAccount;
import io.onedev.server.model.SsoProvider;
import io.onedev.server.persistence.annotation.Sessional;
import io.onedev.server.persistence.annotation.Transactional;
import io.onedev.server.persistence.dao.BaseEntityManager;
import io.onedev.server.persistence.dao.Dao;
import io.onedev.server.persistence.dao.EntityCriteria;
@Singleton
public class DefaultSsoAccountManager extends BaseEntityManager<SsoAccount> implements SsoAccountManager {
@Inject
public DefaultSsoAccountManager(Dao dao) {
super(dao);
}
@Transactional
@Override
public void create(SsoAccount ssoAccount) {
dao.persist(ssoAccount);
}
@Sessional
@Override
public SsoAccount find(SsoProvider provider, String subject) {
var criteria = EntityCriteria.of(SsoAccount.class);
criteria.add(Restrictions.eq(SsoAccount.PROP_PROVIDER, provider));
criteria.add(Restrictions.eq(SsoAccount.PROP_SUBJECT, subject));
return dao.find(criteria);
}
}

View File

@ -0,0 +1,49 @@
package io.onedev.server.entitymanager.impl;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Restrictions;
import io.onedev.server.entitymanager.SsoProviderManager;
import io.onedev.server.model.SsoProvider;
import io.onedev.server.persistence.annotation.Sessional;
import io.onedev.server.persistence.annotation.Transactional;
import io.onedev.server.persistence.dao.BaseEntityManager;
import io.onedev.server.persistence.dao.Dao;
import io.onedev.server.persistence.dao.EntityCriteria;
@Singleton
public class DefaultSsoProviderManager extends BaseEntityManager<SsoProvider> implements SsoProviderManager {
@Inject
public DefaultSsoProviderManager(Dao dao) {
super(dao);
}
@Transactional
@Override
public void createOrUpdate(SsoProvider ssoProvider) {
dao.persist(ssoProvider);
}
@Sessional
@Override
public SsoProvider find(String name) {
var criteria = EntityCriteria.of(SsoProvider.class);
criteria.add(Restrictions.eq(SsoProvider.PROP_NAME, name));
return dao.find(criteria);
}
@Sessional
public List<SsoProvider> query() {
var criteria = EntityCriteria.of(SsoProvider.class);
criteria.addOrder(Order.asc(SsoProvider.PROP_NAME));
criteria.setCacheable(true);
return query(criteria);
}
}

View File

@ -316,6 +316,8 @@ public class DefaultUserManager extends BaseEntityManager<User> implements UserM
dao.remove(vote);
for (var stopwatch: user.getStopwatches())
dao.remove(stopwatch);
for (var ssoAccount: user.getSsoAccounts())
dao.remove(ssoAccount);
for (var personalization: user.getIssueQueryPersonalizations())
dao.remove(personalization);
@ -371,6 +373,8 @@ public class DefaultUserManager extends BaseEntityManager<User> implements UserM
dao.remove(vote);
for (var stopwatch: user.getStopwatches())
dao.remove(stopwatch);
for (var ssoAccount: user.getSsoAccounts())
dao.remove(ssoAccount);
for (var personalization: user.getIssueQueryPersonalizations())
dao.remove(personalization);

View File

@ -18,7 +18,7 @@ public class Setting extends AbstractEntity {
public static final String PROP_KEY = "key";
public enum Key {SYSTEM, MAIL_SERVICE, BACKUP, SECURITY, AUTHENTICATOR, ISSUE, JOB_EXECUTORS,
GROOVY_SCRIPTS, PULL_REQUEST, BUILD, PACK, PROJECT, SSH, GPG, SSO_CONNECTORS,
GROOVY_SCRIPTS, PULL_REQUEST, BUILD, PACK, PROJECT, SSH, GPG,
EMAIL_TEMPLATES, CONTRIBUTED_SETTINGS, SERVICE_DESK_SETTING,
AGENT, PERFORMANCE, BRANDING, CLUSTER_SETTING, SUBSCRIPTION_DATA, ALERT,
SYSTEM_UUID, AUDIT

View File

@ -0,0 +1,66 @@
package io.onedev.server.model;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Index;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
@Entity
@Table(
indexes={@Index(columnList="o_user_id"), @Index(columnList="o_provider_id"), @Index(columnList=SsoAccount.PROP_SUBJECT)},
uniqueConstraints={@UniqueConstraint(columnNames={"o_provider_id", SsoAccount.PROP_SUBJECT})
})
@Cache(usage=CacheConcurrencyStrategy.READ_WRITE)
public class SsoAccount extends AbstractEntity {
private static final long serialVersionUID = 1L;
public static final String PROP_USER = "user";
public static final String PROP_PROVIDER = "provider";
public static final String PROP_SUBJECT = "subject";
@ManyToOne(fetch=FetchType.LAZY)
@JoinColumn(nullable=false)
private User user;
@ManyToOne(fetch=FetchType.LAZY)
@JoinColumn(nullable=false)
private SsoProvider provider;
@Column(nullable=false)
private String subject;
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public SsoProvider getProvider() {
return provider;
}
public void setProvider(SsoProvider provider) {
this.provider = provider;
}
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
}

View File

@ -0,0 +1,67 @@
package io.onedev.server.model;
import java.util.ArrayList;
import java.util.Collection;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Index;
import javax.persistence.Lob;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
import io.onedev.server.model.support.administration.sso.SsoConnector;
@Entity
@Table(indexes={@Index(columnList=SsoProvider.PROP_NAME)})
public class SsoProvider extends AbstractEntity {
private static final long serialVersionUID = 1L;
public static final String PROP_NAME = "name";
@Column(nullable=false, unique=true)
private String name;
@ManyToOne(fetch=FetchType.LAZY)
private Group defaultGroup;
@Lob
@Column(nullable=false, length=65535)
private SsoConnector connector;
@OneToMany(mappedBy="provider", cascade=CascadeType.REMOVE)
@Cache(usage=CacheConcurrencyStrategy.READ_WRITE)
private Collection<SsoAccount> accounts = new ArrayList<>();
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Group getDefaultGroup() {
return defaultGroup;
}
public void setDefaultGroup(Group defaultGroup) {
this.defaultGroup = defaultGroup;
}
public SsoConnector getConnector() {
return connector;
}
public void setConnector(SsoConnector connector) {
this.connector = connector;
}
}

View File

@ -89,6 +89,8 @@ public class User extends AbstractEntity implements AuthenticationInfo {
public static final String PROP_DISABLED = "disabled";
public static final String PROP_NOTIFY_OWN_EVENTS = "notifyOwnEvents";
public static final String PROP_PASSWORD = "password";
private static ThreadLocal<Stack<User>> stack = ThreadLocal.withInitial(() -> new Stack<>());
@ -141,6 +143,10 @@ public class User extends AbstractEntity implements AuthenticationInfo {
@OneToMany(mappedBy="user", cascade=CascadeType.REMOVE)
@Cache(usage=CacheConcurrencyStrategy.READ_WRITE)
private Collection<Membership> memberships = new ArrayList<>();
@OneToMany(mappedBy="user", cascade=CascadeType.REMOVE)
@Cache(usage=CacheConcurrencyStrategy.READ_WRITE)
private Collection<SsoAccount> ssoAccounts = new ArrayList<>();
@OneToMany(mappedBy="user", cascade=CascadeType.REMOVE)
private Collection<PullRequestReview> pullRequestReviews = new ArrayList<>();
@ -646,6 +652,14 @@ public class User extends AbstractEntity implements AuthenticationInfo {
public void setMemberships(Collection<Membership> memberships) {
this.memberships = memberships;
}
public Collection<SsoAccount> getSsoAccounts() {
return ssoAccounts;
}
public void setSsoAccounts(Collection<SsoAccount> ssoAccounts) {
this.ssoAccounts = ssoAccounts;
}
@Override
public String toString() {
@ -1087,6 +1101,13 @@ public class User extends AbstractEntity implements AuthenticationInfo {
publicEmailAddress = Optional.ofNullable(getEmailAddressManager().findPublic(this));
return publicEmailAddress.orElse(null);
}
public void addEmailAddress(EmailAddress emailAddress) {
emailAddress.setOwner(this);
emailAddress.setPrimary(getEmailAddresses().stream().noneMatch(it->it.isPrimary()));
emailAddress.setGit(getEmailAddresses().stream().noneMatch(it->it.isGit()));
getEmailAddresses().add(emailAddress);
}
public UserFacade getFacade() {
return new UserFacade(getId(), getName(), getFullName(), isServiceAccount(), isDisabled());

View File

@ -1,9 +1,13 @@
package io.onedev.server.model.support.administration.authenticator;
import javax.annotation.Nullable;
import java.io.Serializable;
import java.util.Collection;
public class Authenticated {
import javax.annotation.Nullable;
public class Authenticated implements Serializable {
private static final long serialVersionUID = 1L;
private final String fullName;

View File

@ -4,42 +4,30 @@ import java.util.Collection;
import javax.annotation.Nullable;
import org.apache.shiro.authc.AuthenticationToken;
import io.onedev.server.model.support.administration.authenticator.Authenticated;
public class SsoAuthenticated extends Authenticated implements AuthenticationToken {
public class SsoAuthenticated extends Authenticated {
private static final long serialVersionUID = 1L;
private final String subject;
private final String userName;
private final SsoConnector connector;
public SsoAuthenticated(String userName, String email, @Nullable String fullName,
@Nullable Collection<String> groupNames,
@Nullable Collection<String> sshKeys, SsoConnector connector) {
private final String userName;
public SsoAuthenticated(String subject, @Nullable String userName, @Nullable String email, @Nullable String fullName,
@Nullable Collection<String> groupNames, @Nullable Collection<String> sshKeys) {
super(email, fullName, groupNames, sshKeys);
this.subject = subject;
this.userName = userName;
this.connector = connector;
}
public String getSubject() {
return subject;
}
@Nullable
public String getUserName() {
return userName;
}
public SsoConnector getConnector() {
return connector;
}
@Override
public Object getPrincipal() {
return userName;
}
@Override
public Object getCredentials() {
return null;
}
}

View File

@ -1,61 +1,33 @@
package io.onedev.server.model.support.administration.sso;
import io.onedev.server.annotation.Editable;
import io.onedev.server.annotation.GroupChoice;
import io.onedev.server.util.usage.Usage;
import javax.validation.constraints.NotEmpty;
import java.io.Serializable;
import java.net.URI;
import java.net.URISyntaxException;
import io.onedev.server.OneDev;
import io.onedev.server.annotation.Editable;
import io.onedev.server.entitymanager.SettingManager;
import io.onedev.server.web.page.security.SsoProcessPage;
@Editable
public abstract class SsoConnector implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private String defaultGroup;
@Editable(order=100, description="Name of the provider will be displayed on login button")
@NotEmpty
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public abstract String getButtonImageUrl();
@Editable(order=20000, placeholder="No default group", description="Optionally add newly authenticated "
+ "user to specified group if membership information is not available")
@GroupChoice
public String getDefaultGroup() {
return defaultGroup;
}
public void setDefaultGroup(String defaultGroup) {
this.defaultGroup = defaultGroup;
}
public void onRenameGroup(String oldName, String newName) {
if (oldName.equals(defaultGroup))
defaultGroup = newName;
public final URI getCallbackUri(String providerName) {
String serverUrl = OneDev.getInstance(SettingManager.class).getSystemSetting().getServerUrl();
try {
return new URI(serverUrl + "/" + SsoProcessPage.MOUNT_PATH + "/"
+ SsoProcessPage.STAGE_CALLBACK + "/" + providerName);
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}
public Usage onDeleteGroup(String groupName) {
Usage usage = new Usage();
if (groupName.equals(defaultGroup))
usage.add("default group");
return usage;
}
public abstract SsoAuthenticated handleAuthResponse(String providerName);
public abstract URI getCallbackUri();
public abstract SsoAuthenticated processLoginResponse();
public abstract void initiateLogin();
public abstract String buildAuthUrl(String providerName);
}

View File

@ -68,7 +68,7 @@ public class AccessTokenAuthorizationResource {
accessTokenAuthorizationManager.createOrUpdate(authorization);
if (!getAuthUser().equals(owner)) {
var newAuditContent = VersionedXmlDoc.fromBean(authorization).toXML();
auditManager.audit(null, "created access token authorization for account \"" + owner.getName() + "\" via RESTful API", null, newAuditContent);
auditManager.audit(null, "created access token authorization in account \"" + owner.getName() + "\" via RESTful API", null, newAuditContent);
}
return authorization.getId();
}
@ -87,7 +87,7 @@ public class AccessTokenAuthorizationResource {
if (!getAuthUser().equals(owner)) {
var oldAuditContent = authorization.getOldVersion().toXML();
var newAuditContent = VersionedXmlDoc.fromBean(authorization).toXML();
auditManager.audit(null, "changed access token authorization for account \"" + owner.getName() + "\" via RESTful API", oldAuditContent, newAuditContent);
auditManager.audit(null, "changed access token authorization in account \"" + owner.getName() + "\" via RESTful API", oldAuditContent, newAuditContent);
}
return Response.ok().build();
}
@ -103,7 +103,7 @@ public class AccessTokenAuthorizationResource {
accessTokenAuthorizationManager.delete(authorization);
if (!getAuthUser().equals(owner)) {
var oldAuditContent = VersionedXmlDoc.fromBean(authorization).toXML();
auditManager.audit(null, "deleted access token authorization for account \"" + owner.getName() + "\" via RESTful API", oldAuditContent, null);
auditManager.audit(null, "deleted access token authorization from account \"" + owner.getName() + "\" via RESTful API", oldAuditContent, null);
}
return Response.ok().build();
}

View File

@ -82,7 +82,7 @@ public class AccessTokenResource {
if (!getAuthUser().equals(owner)) {
var newAuditContent = VersionedXmlDoc.fromBean(accessToken.getFacade()).toXML();
auditManager.audit(null, "created access token \"" + accessToken.getName() + "\" for account \"" + owner.getName() + "\" via RESTful API",
auditManager.audit(null, "created access token \"" + accessToken.getName() + "\" in account \"" + owner.getName() + "\" via RESTful API",
null, newAuditContent);
}
@ -106,7 +106,7 @@ public class AccessTokenResource {
if (!getAuthUser().equals(owner)) {
var oldAuditContent = accessToken.getOldVersion().toXML();
var newAuditContent = VersionedXmlDoc.fromBean(accessToken).toXML();
auditManager.audit(null, "changed access token \"" + accessToken.getName() + "\" for account \"" + owner.getName() + "\" via RESTful API",
auditManager.audit(null, "changed access token \"" + accessToken.getName() + "\" in account \"" + owner.getName() + "\" via RESTful API",
oldAuditContent, newAuditContent);
}
return Response.ok().build();
@ -123,7 +123,7 @@ public class AccessTokenResource {
if (!getAuthUser().equals(accessToken.getOwner())) {
var oldAuditContent = VersionedXmlDoc.fromBean(accessToken.getFacade()).toXML();
auditManager.audit(null, "deleted access token \"" + accessToken.getName() + "\" for account \"" + accessToken.getOwner().getName() + "\" via RESTful API",
auditManager.audit(null, "deleted access token \"" + accessToken.getName() + "\" from account \"" + accessToken.getOwner().getName() + "\" via RESTful API",
oldAuditContent, null);
}
return Response.ok().build();

View File

@ -84,7 +84,7 @@ public class EmailAddressResource {
emailAddressManager.create(emailAddress);
if (!getAuthUser().equals(owner))
auditManager.audit(null, "added email address \"" + emailAddress.getValue() + "\" for account \"" + owner.getName() + "\" via RESTful API", null, null);
auditManager.audit(null, "added email address \"" + emailAddress.getValue() + "\" in account \"" + owner.getName() + "\" via RESTful API", null, null);
return emailAddress.getId();
}
@ -100,7 +100,7 @@ public class EmailAddressResource {
emailAddressManager.setAsPublic(emailAddress);
if (!getAuthUser().equals(owner))
auditManager.audit(null, "set email address \"" + emailAddress.getValue() + "\" as public for account \"" + owner.getName() + "\" via RESTful API", null, null);
auditManager.audit(null, "set email address \"" + emailAddress.getValue() + "\" as public in account \"" + owner.getName() + "\" via RESTful API", null, null);
return emailAddressId;
}
@ -117,7 +117,7 @@ public class EmailAddressResource {
emailAddressManager.setAsPrivate(emailAddress);
if (!getAuthUser().equals(owner))
auditManager.audit(null, "set email address \"" + emailAddress.getValue() + "\" as private for account \"" + owner.getName() + "\" via RESTful API", null, null);
auditManager.audit(null, "set email address \"" + emailAddress.getValue() + "\" as private in account \"" + owner.getName() + "\" via RESTful API", null, null);
return emailAddressId;
}
@ -137,7 +137,7 @@ public class EmailAddressResource {
emailAddressManager.setAsPrimary(emailAddress);
if (!getAuthUser().equals(owner))
auditManager.audit(null, "set email address \"" + emailAddress.getValue() + "\" as primary for account \"" + owner.getName() + "\" via RESTful API", null, null);
auditManager.audit(null, "set email address \"" + emailAddress.getValue() + "\" as primary in account \"" + owner.getName() + "\" via RESTful API", null, null);
return emailAddressId;
}
@ -153,7 +153,7 @@ public class EmailAddressResource {
emailAddressManager.useForGitOperations(emailAddress);
if (!getAuthUser().equals(emailAddress.getOwner()))
auditManager.audit(null, "specified email address \"" + emailAddress.getValue() + "\" for git operations for account \"" + emailAddress.getOwner().getName() + "\" via RESTful API", null, null);
auditManager.audit(null, "specified email address \"" + emailAddress.getValue() + "\" for git operations in account \"" + emailAddress.getOwner().getName() + "\" via RESTful API", null, null);
return emailAddressId;
}
@ -193,7 +193,7 @@ public class EmailAddressResource {
emailAddressManager.delete(emailAddress);
if (!getAuthUser().equals(emailAddress.getOwner()))
auditManager.audit(null, "deleted email address \"" + emailAddress.getValue() + "\" for account \"" + emailAddress.getOwner().getName() + "\" via RESTful API", null, null);
auditManager.audit(null, "deleted email address \"" + emailAddress.getValue() + "\" from account \"" + emailAddress.getOwner().getName() + "\" via RESTful API", null, null);
return Response.ok().build();
}

View File

@ -38,7 +38,6 @@ import io.onedev.server.model.support.administration.authenticator.Authenticator
import io.onedev.server.model.support.administration.emailtemplates.EmailTemplates;
import io.onedev.server.model.support.administration.jobexecutor.JobExecutor;
import io.onedev.server.model.support.administration.mailservice.MailService;
import io.onedev.server.model.support.administration.sso.SsoConnector;
import io.onedev.server.rest.annotation.Api;
import io.onedev.server.security.SecurityUtils;
import io.onedev.server.web.page.layout.ContributedAdministrationSetting;
@ -184,16 +183,7 @@ public class SettingResource {
throw new UnauthorizedException();
return settingManager.getSshSetting();
}
@Api(order=1400)
@Path("/sso-connectors")
@GET
public List<SsoConnector> getSsoConnectors() {
if (!SecurityUtils.isAdministrator())
throw new UnauthorizedException();
return settingManager.getSsoConnectors();
}
@Api(order=1450)
@Path("/contributed-settings")
@GET
@ -388,20 +378,7 @@ public class SettingResource {
oldAuditContent, VersionedXmlDoc.fromBean(sshSetting).toXML());
return Response.ok().build();
}
@Api(order=2700)
@Path("/sso-connectors")
@POST
public Response setSsoConnectors(@NotNull List<SsoConnector> ssoConnectors) {
if (!SecurityUtils.isAdministrator())
throw new UnauthorizedException();
var oldAuditContent = VersionedXmlDoc.fromBean(settingManager.getSsoConnectors()).toXML();
settingManager.saveSsoConnectors(ssoConnectors);
auditManager.audit(null, "changed sso connectors via RESTful API",
oldAuditContent, VersionedXmlDoc.fromBean(ssoConnectors).toXML());
return Response.ok().build();
}
private String getAuditContent(Map<String, ContributedAdministrationSetting> contributedSettings) {
var list = new ArrayList<ContributedAdministrationSetting>();
for (var entry: contributedSettings.entrySet())

View File

@ -63,7 +63,7 @@ public class SshKeyResource {
sshKeyManager.create(sshKey);
if (!getAuthUser().equals(sshKey.getOwner())) {
var newAuditContent = VersionedXmlDoc.fromBean(sshKey).toXML();
auditManager.audit(null, "created ssh key for account \"" + sshKey.getOwner().getName() + "\" via RESTful API", null, newAuditContent);
auditManager.audit(null, "created ssh key in account \"" + sshKey.getOwner().getName() + "\" via RESTful API", null, newAuditContent);
}
return sshKey.getId();
}
@ -78,7 +78,7 @@ public class SshKeyResource {
sshKeyManager.delete(sshKey);
if (!getAuthUser().equals(sshKey.getOwner())) {
var oldAuditContent = VersionedXmlDoc.fromBean(sshKey).toXML();
auditManager.audit(null, "deleted ssh key for account \"" + sshKey.getOwner().getName() + "\" via RESTful API", oldAuditContent, null);
auditManager.audit(null, "deleted ssh key from account \"" + sshKey.getOwner().getName() + "\" via RESTful API", oldAuditContent, null);
}
return Response.ok().build();
}

View File

@ -1,8 +1,9 @@
package io.onedev.server.security.realm;
import static io.onedev.server.validation.validator.UserNameValidator.normalizeUserName;
import static io.onedev.server.web.translation.Translation._T;
import java.util.Collection;
import java.text.MessageFormat;
import java.util.HashSet;
import javax.annotation.Nullable;
@ -21,8 +22,6 @@ import org.apache.shiro.realm.AuthenticatingRealm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.Sets;
import io.onedev.commons.utils.ExceptionUtils;
import io.onedev.server.entitymanager.EmailAddressManager;
import io.onedev.server.entitymanager.GroupManager;
@ -77,11 +76,10 @@ public class PasswordAuthenticatingRealm extends AuthenticatingRealm {
return token instanceof UsernamePasswordToken;
}
private User newUser(String userName, Authenticated authenticated, @Nullable String defaultGroup) {
private User newUser(String userName, Authenticated authenticated, @Nullable String defaultGroupName) {
User user = new User();
user.setName(userName);
if (authenticated.getFullName() != null)
user.setFullName(authenticated.getFullName());
user.setFullName(authenticated.getFullName());
userManager.create(user);
if (authenticated.getEmail() != null) {
@ -96,54 +94,43 @@ public class PasswordAuthenticatingRealm extends AuthenticatingRealm {
user.getEmailAddresses().add(emailAddress);
}
Collection<String> groupNames = authenticated.getGroupNames();
if (groupNames == null && defaultGroup != null)
groupNames = Sets.newHashSet(defaultGroup);
var defaultLoginGroupName = settingManager.getSecuritySetting().getDefaultGroupName();
if (defaultLoginGroupName != null) {
if (groupNames == null)
groupNames = new HashSet<>();
groupNames.add(defaultLoginGroupName);
}
if (groupNames != null)
membershipManager.syncMemberships(user, groupNames);
if (authenticated.getSshKeys() != null)
sshKeyManager.syncSshKeys(user, authenticated.getSshKeys());
syncGroupsAndSshKeys(user, true, authenticated, defaultGroupName);
return user;
}
private void syncGroupsAndSshKeys(User user, boolean forNewUser,
Authenticated authenticated, @Nullable String defaultGroupName) {
var groupNames = authenticated.getGroupNames();
if (forNewUser && groupNames == null)
groupNames = new HashSet<String>();
if (groupNames != null) {
if (defaultGroupName != null)
groupNames.add(defaultGroupName);
if (settingManager.getSecuritySetting().getDefaultGroupName() != null)
groupNames.add(settingManager.getSecuritySetting().getDefaultGroupName());
membershipManager.syncMemberships(user, groupNames);
}
if (authenticated.getSshKeys() != null)
sshKeyManager.syncSshKeys(user, authenticated.getSshKeys());
}
@Transactional
protected void updateUser(User user, Authenticated authenticated, @Nullable EmailAddress emailAddress) {
protected void updateUser(User user, Authenticated authenticated,
@Nullable EmailAddress emailAddress, @Nullable String defaultGroupName) {
if (emailAddress != null) {
emailAddress.setVerificationCode(null);
emailAddressManager.setAsPrimary(emailAddress);
if (!user.equals(emailAddress.getOwner()))
user.addEmailAddress(emailAddress);
emailAddressManager.update(emailAddress);
} else if (authenticated.getEmail() != null) {
for (var eachEmailAddress: user.getEmailAddresses()) {
eachEmailAddress.setPrimary(false);
emailAddressManager.update(eachEmailAddress);
}
emailAddress = new EmailAddress();
emailAddress.setValue(authenticated.getEmail());
emailAddress.setVerificationCode(null);
emailAddress.setOwner(user);
emailAddress.setPrimary(true);
emailAddress.setGit(true);
user.addEmailAddress(emailAddress);
emailAddressManager.create(emailAddress);
}
if (authenticated.getFullName() != null)
user.setFullName(authenticated.getFullName());
userManager.update(user, null);
var groupNames = authenticated.getGroupNames();
if (groupNames != null) {
var defaultLoginGroupName = settingManager.getSecuritySetting().getDefaultGroupName();
if (defaultLoginGroupName != null)
groupNames.add(defaultLoginGroupName);
membershipManager.syncMemberships(user, groupNames);
}
if (authenticated.getSshKeys() != null)
sshKeyManager.syncSshKeys(user, authenticated.getSshKeys());
syncGroupsAndSshKeys(user, false, authenticated, defaultGroupName);
}
@Override
@ -157,9 +144,9 @@ public class PasswordAuthenticatingRealm extends AuthenticatingRealm {
user = userManager.findByName(userName);
if (user != null) {
if (user.isDisabled())
throw new DisabledAccountException("Account is disabled");
throw new DisabledAccountException(_T("Account is disabled"));
else if (user.isServiceAccount())
throw new DisabledAccountException("Service account not allowed to login");
throw new DisabledAccountException(_T("Service account not allowed to login"));
if (user.getPassword() == null) {
var authenticator = settingManager.getAuthenticator();
if (authenticator != null) {
@ -168,27 +155,22 @@ public class PasswordAuthenticatingRealm extends AuthenticatingRealm {
if (emailAddressValue != null) {
var emailAddress = emailAddressManager.findByValue(emailAddressValue);
if (emailAddress != null) {
if (emailAddress.getOwner().equals(user)) {
updateUser(user, authenticated, emailAddress);
return user;
} else if (!emailAddress.isVerified()) {
emailAddress.setOwner(user);
updateUser(user, authenticated, emailAddress);
if (emailAddress.getOwner().equals(user) || !emailAddress.isVerified()) {
updateUser(user, authenticated, emailAddress, authenticator.getDefaultGroup());
return user;
} else {
throw new AuthenticationException("Email address '" + emailAddressValue
+ "' has already been used by another user");
throw new AuthenticationException(MessageFormat.format(_T("Email address \"{0}\" already used by another account"), emailAddressValue));
}
} else {
updateUser(user, authenticated, null);
updateUser(user, authenticated, null, authenticator.getDefaultGroup());
return user;
}
} else {
updateUser(user, authenticated, null);
updateUser(user, authenticated, null, authenticator.getDefaultGroup());
return user;
}
} else {
throw new AuthenticationException("No external authenticator to authenticate user '" + userName + "'");
throw new AuthenticationException(MessageFormat.format(_T("No external password authenticator to authenticate user \"{0}\""), userName));
}
} else {
return user;
@ -205,8 +187,7 @@ public class PasswordAuthenticatingRealm extends AuthenticatingRealm {
emailAddressManager.delete(emailAddress);
return newUser(userName, authenticated, authenticator.getDefaultGroup());
} else {
throw new AuthenticationException("Email address '" + emailAddressValue
+ "' has already been used by another user");
throw new AuthenticationException(MessageFormat.format(_T("Email address \"{0}\" already used by another account"), emailAddressValue));
}
} else {
return newUser(userName, authenticated, authenticator.getDefaultGroup());
@ -215,7 +196,7 @@ public class PasswordAuthenticatingRealm extends AuthenticatingRealm {
return newUser(userName, authenticated, authenticator.getDefaultGroup());
}
} else {
throw new UnknownAccountException("Invalid credentials");
throw new UnknownAccountException(_T("Invalid credentials"));
}
}
} catch (Exception e) {
@ -224,7 +205,7 @@ public class PasswordAuthenticatingRealm extends AuthenticatingRealm {
throw ExceptionUtils.unchecked(e);
} else {
logger.error("Error authenticating user", e);
throw new AuthenticationException("Error authenticating user", e);
throw new AuthenticationException(_T("Error authenticating user"), e);
}
}
});

View File

@ -1,146 +0,0 @@
package io.onedev.server.security.realm;
import static io.onedev.server.validation.validator.UserNameValidator.suggestUserName;
import java.util.Collection;
import java.util.HashSet;
import java.util.concurrent.Callable;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.credential.AllowAllCredentialsMatcher;
import org.apache.shiro.realm.AuthenticatingRealm;
import com.google.common.collect.Sets;
import io.onedev.server.entitymanager.EmailAddressManager;
import io.onedev.server.entitymanager.GroupManager;
import io.onedev.server.entitymanager.MembershipManager;
import io.onedev.server.entitymanager.ProjectManager;
import io.onedev.server.entitymanager.SettingManager;
import io.onedev.server.entitymanager.SshKeyManager;
import io.onedev.server.entitymanager.UserManager;
import io.onedev.server.model.EmailAddress;
import io.onedev.server.model.User;
import io.onedev.server.model.support.administration.sso.SsoAuthenticated;
import io.onedev.server.persistence.SessionManager;
import io.onedev.server.persistence.TransactionManager;
@Singleton
public class SsoAuthenticatingRealm extends AuthenticatingRealm {
private final UserManager userManager;
private final MembershipManager membershipManager;
private final TransactionManager transactionManager;
private final SshKeyManager sshKeyManager;
private final EmailAddressManager emailAddressManager;
private final SettingManager settingManager;
@Inject
public SsoAuthenticatingRealm(UserManager userManager, MembershipManager membershipManager,
GroupManager groupManager, ProjectManager projectManager, SessionManager sessionManager,
TransactionManager transactionManager, SshKeyManager sshKeyManager,
SettingManager settingManager, EmailAddressManager emailAddressManager) {
setCredentialsMatcher(new AllowAllCredentialsMatcher());
this.userManager = userManager;
this.membershipManager = membershipManager;
this.transactionManager = transactionManager;
this.sshKeyManager = sshKeyManager;
this.emailAddressManager = emailAddressManager;
this.settingManager = settingManager;
}
private User newUser(SsoAuthenticated authenticated) {
User user = new User();
user.setName(suggestUserName(authenticated.getUserName()));
if (authenticated.getFullName() != null)
user.setFullName(authenticated.getFullName());
userManager.create(user);
EmailAddress emailAddress = new EmailAddress();
emailAddress.setOwner(user);
emailAddress.setVerificationCode(null);
emailAddress.setValue(authenticated.getEmail());
emailAddress.setPrimary(true);
emailAddress.setGit(true);
emailAddressManager.create(emailAddress);
user.getEmailAddresses().add(emailAddress);
Collection<String> groupNames = authenticated.getGroupNames();
if (groupNames == null && authenticated.getConnector().getDefaultGroup() != null)
groupNames = Sets.newHashSet(authenticated.getConnector().getDefaultGroup());
var defaultLoginGroupName = settingManager.getSecuritySetting().getDefaultGroupName();
if (defaultLoginGroupName != null) {
if (groupNames == null)
groupNames = new HashSet<>();
groupNames.add(defaultLoginGroupName);
}
if (groupNames != null)
membershipManager.syncMemberships(user, groupNames);
if (authenticated.getSshKeys() != null)
sshKeyManager.syncSshKeys(user, authenticated.getSshKeys());
return user;
}
private void updateUser(EmailAddress emailAddress, SsoAuthenticated authenticated) {
User user = emailAddress.getOwner();
if (authenticated.getFullName() != null)
user.setFullName(authenticated.getFullName());
userManager.update(user, null);
emailAddressManager.setAsPrimary(emailAddress);
var groupNames = authenticated.getGroupNames();
if (groupNames != null) {
var defaultLoginGroupName = settingManager.getSecuritySetting().getDefaultGroupName();
if (defaultLoginGroupName != null)
groupNames.add(defaultLoginGroupName);
membershipManager.syncMemberships(user, groupNames);
}
if (authenticated.getSshKeys() != null)
sshKeyManager.syncSshKeys(user, authenticated.getSshKeys());
}
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof SsoAuthenticated;
}
@Override
protected final AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException {
return transactionManager.call((Callable<AuthenticationInfo>) () -> {
var authenticated = (SsoAuthenticated) token;
var emailAddressValue = authenticated.getEmail();
var emailAddress = emailAddressManager.findByValue(emailAddressValue);
if (emailAddress != null) {
if (emailAddress.isVerified()) {
var user = emailAddress.getOwner();
if (user.isDisabled())
throw new AuthenticationException("User is disabled");
updateUser(emailAddress, authenticated);
return user;
} else {
emailAddressManager.delete(emailAddress);
return newUser(authenticated);
}
} else {
return newUser(authenticated);
}
});
}
}

View File

@ -67,7 +67,7 @@ public class DefaultOAuthTokenManager implements OAuthTokenManager, SchedulableT
TokenResponse response;
try {
TokenRequest request = new TokenRequest(new URI(tokenEndpoint), clientAuth, refreshTokenGrant);
TokenRequest request = new TokenRequest(new URI(tokenEndpoint), clientAuth, refreshTokenGrant, null);
response = TokenResponse.parse(request.toHTTPRequest().send());
} catch (ParseException | URISyntaxException | IOException e) {
throw new RuntimeException(e);

View File

@ -72,8 +72,8 @@ import io.onedev.server.web.mapper.BaseResourceMapper;
import io.onedev.server.web.mapper.BaseUrlMapper;
import io.onedev.server.web.page.HomePage;
import io.onedev.server.web.page.base.BasePage;
import io.onedev.server.web.page.simple.error.GeneralErrorPage;
import io.onedev.server.web.page.simple.error.InUseErrorPage;
import io.onedev.server.web.page.error.GeneralErrorPage;
import io.onedev.server.web.page.error.InUseErrorPage;
import io.onedev.server.web.resource.SpriteResourceReference;
import io.onedev.server.web.resource.SpriteResourceStream;
import io.onedev.server.web.resourcebundle.ResourceBundleReferences;

View File

@ -16,7 +16,6 @@ import org.apache.wicket.request.Request;
import org.apache.wicket.util.collections.ConcurrentHashSet;
import io.onedev.server.OneDev;
import io.onedev.server.web.page.admin.ssosetting.SsoProcessPage;
import io.onedev.server.web.util.Cursor;
public class WebSession extends org.apache.wicket.protocol.http.WebSession {
@ -60,7 +59,6 @@ public class WebSession extends org.apache.wicket.protocol.http.WebSession {
public void logout() {
SecurityUtils.getSubject().logout();
replaceSession();
SsoProcessPage.clearConnectorCookie();
}
@Nullable

View File

@ -61,7 +61,7 @@ import io.onedev.server.web.component.issue.activities.activity.IssueChangeActiv
import io.onedev.server.web.component.issue.activities.activity.IssueCommentActivity;
import io.onedev.server.web.component.issue.activities.activity.IssueWorkActivity;
import io.onedev.server.web.page.base.BasePage;
import io.onedev.server.web.page.simple.security.LoginPage;
import io.onedev.server.web.page.security.LoginPage;
import io.onedev.server.web.util.WicketUtils;
public abstract class IssueActivitiesPanel extends Panel {

View File

@ -85,7 +85,7 @@ import io.onedev.server.web.component.user.ident.UserIdentPanel;
import io.onedev.server.web.component.user.list.SimpleUserListLink;
import io.onedev.server.web.page.base.BasePage;
import io.onedev.server.web.page.project.issues.iteration.IterationIssuesPage;
import io.onedev.server.web.page.simple.security.LoginPage;
import io.onedev.server.web.page.security.LoginPage;
public abstract class IssueSidePanel extends Panel {

View File

@ -20,6 +20,10 @@ public abstract class ActionTab extends Tab {
this.titleModel = titleModel;
this.iconModel = iconModel;
}
public ActionTab(IModel<String> titleModel) {
this(titleModel, null);
}
protected final IModel<String> getTitleModel() {
return titleModel;

View File

@ -103,7 +103,7 @@ abstract class AccessTokenEditPanel extends Panel {
if (getPage() instanceof UserPage) {
var newAuditContent = VersionedXmlDoc.fromBean(bean).toXML();
var verb = oldAuditContent != null ? "changed" : "created";
getAuditManager().audit(null, verb + " access token \"" + token.getName() + "\" for account \"" + token.getOwner().getName() + "\"", oldAuditContent, newAuditContent);
getAuditManager().audit(null, verb + " access token \"" + token.getName() + "\" in account \"" + token.getOwner().getName() + "\"", oldAuditContent, newAuditContent);
oldAuditContent = newAuditContent;
}
});

View File

@ -57,7 +57,7 @@ public abstract class AccessTokenListPanel extends Panel {
getTokenManager().delete(token);
if (getPage() instanceof UserPage) {
var oldAuditContent = VersionedXmlDoc.fromBean(token).toXML();
getAuditManager().audit(null, "deleted access token \"" + token.getName() + "\" for account \"" + token.getOwner().getName() + "\"", oldAuditContent, null);
getAuditManager().audit(null, "deleted access token \"" + token.getName() + "\" from account \"" + token.getOwner().getName() + "\"", oldAuditContent, null);
}
target.add(container);
}

View File

@ -58,7 +58,7 @@ abstract class AccessTokenPanel extends Panel {
var newAuditContent = VersionedXmlDoc.fromBean(token).toXML();
OneDev.getInstance(AccessTokenManager.class).createOrUpdate(token);
if (getPage() instanceof UserPage) {
OneDev.getInstance(AuditManager.class).audit(null, "regenerated access token \"" + token.getName() + "\" for account \"" + token.getOwner().getName() + "\"", oldAuditContent, newAuditContent);
OneDev.getInstance(AuditManager.class).audit(null, "regenerated access token \"" + token.getName() + "\" in account \"" + token.getOwner().getName() + "\"", oldAuditContent, newAuditContent);
}
target.add(AccessTokenPanel.this);
Session.get().success(_T("Access token regenerated successfully"));

View File

@ -57,7 +57,7 @@ public class AvatarEditPanel extends GenericPanel<User> {
public void onClick() {
getAvatarManager().useUserAvatar(getUser().getId(), null);
if (getPage() instanceof UserPage)
getAuditManager().audit(null, "specified to use default avatar for account \"" + getUser().getName() + "\"", null, null);
getAuditManager().audit(null, "specified to use default avatar in account \"" + getUser().getName() + "\"", null, null);
setResponsePage(getPage().getClass(), getPage().getPageParameters());
}
@ -74,7 +74,7 @@ public class AvatarEditPanel extends GenericPanel<User> {
AvatarManager avatarManager = OneDev.getInstance(AvatarManager.class);
avatarManager.useUserAvatar(getUser().getId(), uploadedAvatarData);
if (getPage() instanceof UserPage)
getAuditManager().audit(null, "specified to use uploaded avatar for account \"" + getUser().getName() + "\"", null, null);
getAuditManager().audit(null, "specified to use uploaded avatar in account \"" + getUser().getName() + "\"", null, null);
setResponsePage(getPage().getClass(), getPage().getPageParameters());
}

View File

@ -102,7 +102,7 @@ public class EmailAddressesPanel extends GenericPanel<User> {
var emailAddress = getEmailAddressManager().load(emailAddressId);
getEmailAddressManager().setAsPrimary(emailAddress);
if (getPage() instanceof UserPage)
getAuditManager().audit(null, "specified email address \"" + emailAddress.getValue() + "\" as primary for account \"" + getUser().getName() + "\"", null, null);
getAuditManager().audit(null, "specified email address \"" + emailAddress.getValue() + "\" as primary in account \"" + getUser().getName() + "\"", null, null);
}
};
@ -127,7 +127,7 @@ public class EmailAddressesPanel extends GenericPanel<User> {
var emailAddress = getEmailAddressManager().load(emailAddressId);
getEmailAddressManager().useForGitOperations(emailAddress);
if (getPage() instanceof UserPage)
getAuditManager().audit(null, "specified email address \"" + emailAddress.getValue() + "\" for git operations for account \"" + getUser().getName() + "\"", null, null);
getAuditManager().audit(null, "specified email address \"" + emailAddress.getValue() + "\" for git operations in account \"" + getUser().getName() + "\"", null, null);
}
};
@ -152,7 +152,7 @@ public class EmailAddressesPanel extends GenericPanel<User> {
var emailAddress = getEmailAddressManager().load(emailAddressId);
getEmailAddressManager().setAsPublic(emailAddress);
if (getPage() instanceof UserPage)
getAuditManager().audit(null, "specified email address \"" + emailAddress.getValue() + "\" as public for account \"" + getUser().getName() + "\"", null, null);
getAuditManager().audit(null, "specified email address \"" + emailAddress.getValue() + "\" as public in account \"" + getUser().getName() + "\"", null, null);
}
};
@ -176,7 +176,7 @@ public class EmailAddressesPanel extends GenericPanel<User> {
var emailAddress = getEmailAddressManager().load(emailAddressId);
getEmailAddressManager().setAsPrivate(emailAddress);
if (getPage() instanceof UserPage)
getAuditManager().audit(null, "specified email address \"" + emailAddress.getValue() + "\" as private for account \"" + getUser().getName() + "\"", null, null);
getAuditManager().audit(null, "specified email address \"" + emailAddress.getValue() + "\" as private in account \"" + getUser().getName() + "\"", null, null);
}
};
@ -232,7 +232,7 @@ public class EmailAddressesPanel extends GenericPanel<User> {
var emailAddress = getEmailAddressManager().load(emailAddressId);
getEmailAddressManager().delete(emailAddress);
if (getPage() instanceof UserPage)
getAuditManager().audit(null, "deleted email address \"" + emailAddress.getValue() + "\" for account \"" + getUser().getName() + "\"", null, null);
getAuditManager().audit(null, "deleted email address \"" + emailAddress.getValue() + "\" from account \"" + getUser().getName() + "\"", null, null);
} else {
Session.get().warn(_T("At least one email address should be configured, please add a new one first"));
}
@ -275,7 +275,7 @@ public class EmailAddressesPanel extends GenericPanel<User> {
address.setVerificationCode(null);
getEmailAddressManager().create(address);
if (getPage() instanceof UserPage)
getAuditManager().audit(null, "added email address \"" + address.getValue() + "\" for account \"" + getUser().getName() + "\"", null, null);
getAuditManager().audit(null, "added email address \"" + address.getValue() + "\" in account \"" + getUser().getName() + "\"", null, null);
emailAddressValue = null;
}
}

View File

@ -138,7 +138,7 @@ public class GpgKeyListPanel extends GenericPanel<List<GpgKey>> {
GpgKey gpgKey = rowModel.getObject();
OneDev.getInstance(GpgKeyManager.class).delete(gpgKey);
if (getPage() instanceof UserPage)
OneDev.getInstance(AuditManager.class).audit(null, "deleted GPG key \"" + GpgUtils.getKeyIDString(gpgKey.getKeyId()) + "\" for account \"" + gpgKey.getOwner().getName() + "\"", null, null);
OneDev.getInstance(AuditManager.class).audit(null, "deleted GPG key \"" + GpgUtils.getKeyIDString(gpgKey.getKeyId()) + "\" from account \"" + gpgKey.getOwner().getName() + "\"", null, null);
Session.get().success(_T("GPG key deleted"));
target.add(gpgKeysTable);
}

View File

@ -80,7 +80,7 @@ public abstract class InsertGpgKeyPanel extends Panel {
gpgKey.setCreatedAt(new Date());
gpgKeyManager.create(gpgKey);
if (getPage() instanceof UserPage)
OneDev.getInstance(AuditManager.class).audit(null, "added GPG key \"" + GpgUtils.getKeyIDString(gpgKey.getKeyId()) + "\" for account \"" + gpgKey.getOwner().getName() + "\"", null, null);
OneDev.getInstance(AuditManager.class).audit(null, "added GPG key \"" + GpgUtils.getKeyIDString(gpgKey.getKeyId()) + "\" in account \"" + gpgKey.getOwner().getName() + "\"", null, null);
onSave(target);
}
}

View File

@ -54,11 +54,11 @@ public class PasswordEditPanel extends GenericPanel<User> {
var auditManager = OneDev.getInstance(AuditManager.class);
if (getUser().getPassword() != null) {
if (getPage() instanceof UserPage)
auditManager.audit(null, "changed password for account \"" + getUser().getName() + "\"", null, null);
auditManager.audit(null, "changed password in account \"" + getUser().getName() + "\"", null, null);
Session.get().success(_T("Password has been changed"));
} else {
if (getPage() instanceof UserPage)
auditManager.audit(null, "created password for account \"" + getUser().getName() + "\"", null, null);
auditManager.audit(null, "created password in account \"" + getUser().getName() + "\"", null, null);
Session.get().success(_T("Password has been set"));
}

View File

@ -100,14 +100,14 @@ class BuildQueryWatchesPanel extends GenericPanel<User> {
if (queryInfo.projectId == null) {
getUser().getBuildQuerySubscriptions().remove(queryInfo.name);
if (getPage() instanceof UserPage)
auditManager.audit(null, "unsubscribed from build query \"" + queryInfo.name + "\" for account \"" + getUser().getName() + "\"", null, null);
auditManager.audit(null, "unsubscribed from build query \"" + queryInfo.name + "\" in account \"" + getUser().getName() + "\"", null, null);
} else {
for (var personalization: getUser().getBuildQueryPersonalizations()) {
if (personalization.getProject().getId().equals(queryInfo.projectId)) {
personalization.getQuerySubscriptions().remove(queryInfo.name);
getBuildQueryPersonalizationManager().createOrUpdate(personalization);
if (getPage() instanceof UserPage)
auditManager.audit(null, "unsubscribed from build query \"" + queryInfo.name + "\" for account \"" + getUser().getName() + "\" in project \"" + personalization.getProject().getPath() + "\"", null, null);
auditManager.audit(null, "unsubscribed from build query \"" + queryInfo.name + "\" in account \"" + getUser().getName() + "\" in project \"" + personalization.getProject().getPath() + "\"", null, null);
}
}
}

View File

@ -89,7 +89,7 @@ class CommitQueryWatchesPanel extends GenericPanel<User> {
personalization.getQuerySubscriptions().remove(queryInfo.name);
getCommitQueryPersonalizationManager().createOrUpdate(personalization);
if (getPage() instanceof UserPage)
auditManager.audit(null, "unsubscribed from commit query \"" + queryInfo.name + "\" for account \"" + getUser().getName() + "\" in project \"" + personalization.getProject().getPath() + "\"", null, null);
auditManager.audit(null, "unsubscribed from commit query \"" + queryInfo.name + "\" in account \"" + getUser().getName() + "\" in project \"" + personalization.getProject().getPath() + "\"", null, null);
}
}
}

View File

@ -100,14 +100,14 @@ class IssueQueryWatchesPanel extends GenericPanel<User> {
if (queryInfo.projectId == null) {
getUser().getIssueQueryWatches().remove(queryInfo.name);
if (getPage() instanceof UserPage)
auditManager.audit(null, "unwatched issue query \"" + queryInfo.name + "\" for account \"" + getUser().getName() + "\"", null, null);
auditManager.audit(null, "unwatched issue query \"" + queryInfo.name + "\" in account \"" + getUser().getName() + "\"", null, null);
} else {
for (var personalization: getUser().getIssueQueryPersonalizations()) {
if (personalization.getProject().getId().equals(queryInfo.projectId)) {
personalization.getQueryWatches().remove(queryInfo.name);
getIssueQueryPersonalizationManager().createOrUpdate(personalization);
if (getPage() instanceof UserPage)
auditManager.audit(null, "unwatched issue query \"" + queryInfo.name + "\" for account \"" + getUser().getName() + "\" in project \"" + personalization.getProject().getPath() + "\"", null, null);
auditManager.audit(null, "unwatched issue query \"" + queryInfo.name + "\" in account \"" + getUser().getName() + "\" in project \"" + personalization.getProject().getPath() + "\"", null, null);
}
}
}

View File

@ -100,14 +100,14 @@ class PackQueryWatchesPanel extends GenericPanel<User> {
if (queryInfo.projectId == null) {
getUser().getPackQuerySubscriptions().remove(queryInfo.name);
if (getPage() instanceof UserPage)
auditManager.audit(null, "unwatched pack query \"" + queryInfo.name + "\" for account \"" + getUser().getName() + "\"", null, null);
auditManager.audit(null, "unwatched pack query \"" + queryInfo.name + "\" in account \"" + getUser().getName() + "\"", null, null);
} else {
for (var personalization: getUser().getPackQueryPersonalizations()) {
if (personalization.getProject().getId().equals(queryInfo.projectId)) {
personalization.getQuerySubscriptions().remove(queryInfo.name);
getPackQueryPersonalizationManager().createOrUpdate(personalization);
if (getPage() instanceof UserPage)
auditManager.audit(null, "unwatched pack query \"" + queryInfo.name + "\" for account \"" + getUser().getName() + "\" in project \"" + personalization.getProject().getPath() + "\"", null, null);
auditManager.audit(null, "unwatched pack query \"" + queryInfo.name + "\" in account \"" + getUser().getName() + "\" in project \"" + personalization.getProject().getPath() + "\"", null, null);
}
}
}

View File

@ -100,14 +100,14 @@ class PullRequestQueryWatchesPanel extends GenericPanel<User> {
if (queryInfo.projectId == null) {
getUser().getPullRequestQueryWatches().remove(queryInfo.name);
if (getPage() instanceof UserPage)
auditManager.audit(null, "unwatched pull request query \"" + queryInfo.name + "\" for account \"" + getUser().getName() + "\"", null, null);
auditManager.audit(null, "unwatched pull request query \"" + queryInfo.name + "\" in account \"" + getUser().getName() + "\"", null, null);
} else {
for (var personalization: getUser().getPullRequestQueryPersonalizations()) {
if (personalization.getProject().getId().equals(queryInfo.projectId)) {
personalization.getQueryWatches().remove(queryInfo.name);
getPullRequestQueryPersonalizationManager().createOrUpdate(personalization);
if (getPage() instanceof UserPage)
auditManager.audit(null, "unwatched pull request query \"" + queryInfo.name + "\" for account \"" + getUser().getName() + "\" in project \"" + personalization.getProject().getPath() + "\"", null, null);
auditManager.audit(null, "unwatched pull request query \"" + queryInfo.name + "\" in account \"" + getUser().getName() + "\" in project \"" + personalization.getProject().getPath() + "\"", null, null);
}
}
}

View File

@ -63,7 +63,7 @@ public abstract class InsertSshKeyPanel extends Panel {
sshKey.setCreatedAt(new Date());
sshKeyManager.create(sshKey);
if (getPage() instanceof UserPage)
OneDev.getInstance(AuditManager.class).audit(null, "added SSH key \"" + sshKey.getFingerprint() + "\" for account \"" + sshKey.getOwner().getName() + "\"", null, null);
OneDev.getInstance(AuditManager.class).audit(null, "added SSH key \"" + sshKey.getFingerprint() + "\" in account \"" + sshKey.getOwner().getName() + "\"", null, null);
onSave(target);
}
}

View File

@ -114,7 +114,7 @@ public class SshKeyListPanel extends GenericPanel<List<SshKey>> {
SshKey sshKey = rowModel.getObject();
getSshKeyManager().delete(sshKey);
if (getPage() instanceof UserPage)
getAuditManager().audit(null, "deleted SSH key \"" + sshKey.getFingerprint() + "\" for account \"" + sshKey.getOwner().getName() + "\"", null, null);
getAuditManager().audit(null, "deleted SSH key \"" + sshKey.getFingerprint() + "\" in account \"" + sshKey.getOwner().getName() + "\"", null, null);
Session.get().success(_T("SSH key deleted"));
target.add(sshKeysTable);
}

View File

@ -0,0 +1,12 @@
<wicket:panel>
<div class="alert alert-light alert-notice mb-5">
<wicket:svg href="bulb" class="icon mr-2"></wicket:svg>
<wicket:t>Delete SSO account here to unlink user and SSO subject. Note that SSO subject with verified email address
will be linked with user with same verified email address automatically
</wicket:t>
</div>
<table wicket:id="ssoAccounts" class="sso-accounts table table-hover"></table>
<wicket:fragment wicket:id="actionFrag">
<a wicket:id="delete" title="Delete" class="btn btn-xs btn-icon btn-light btn-hover-danger"><wicket:svg href="trash" class="icon"></wicket:svg></a>
</wicket:fragment>
</wicket:panel>

View File

@ -0,0 +1,149 @@
package io.onedev.server.web.component.user.ssoaccount;
import static io.onedev.server.web.translation.Translation._T;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import org.apache.wicket.Session;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.attributes.AjaxRequestAttributes;
import org.apache.wicket.ajax.markup.html.AjaxLink;
import org.apache.wicket.extensions.markup.html.repeater.data.grid.ICellPopulator;
import org.apache.wicket.extensions.markup.html.repeater.data.table.AbstractColumn;
import org.apache.wicket.extensions.markup.html.repeater.data.table.DataTable;
import org.apache.wicket.extensions.markup.html.repeater.data.table.IColumn;
import org.apache.wicket.extensions.markup.html.repeater.util.SortableDataProvider;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.panel.Fragment;
import org.apache.wicket.markup.html.panel.GenericPanel;
import org.apache.wicket.markup.repeater.Item;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.LoadableDetachableModel;
import org.apache.wicket.model.Model;
import io.onedev.server.OneDev;
import io.onedev.server.entitymanager.AuditManager;
import io.onedev.server.entitymanager.SsoAccountManager;
import io.onedev.server.model.SsoAccount;
import io.onedev.server.model.User;
import io.onedev.server.web.ajaxlistener.ConfirmClickListener;
import io.onedev.server.web.component.datatable.DefaultDataTable;
import io.onedev.server.web.page.user.UserPage;
import io.onedev.server.web.util.LoadableDetachableDataProvider;
public class SsoAccountListPanel extends GenericPanel<User> {
private DataTable<SsoAccount, Void> ssoAccountsTable;
public SsoAccountListPanel(String id, IModel<User> model) {
super(id, model);
}
private User getUser() {
return getModelObject();
}
@Override
protected void onInitialize() {
super.onInitialize();
List<IColumn<SsoAccount, Void>> columns = new ArrayList<>();
columns.add(new AbstractColumn<SsoAccount, Void>(Model.of("SSO Provider")) {
@Override
public void populateItem(Item<ICellPopulator<SsoAccount>> cellItem, String componentId,
IModel<SsoAccount> rowModel) {
cellItem.add(new Label(componentId, rowModel.getObject().getProvider().getName()));
}
});
columns.add(new AbstractColumn<SsoAccount, Void>(Model.of("Subject")) {
@Override
public void populateItem(Item<ICellPopulator<SsoAccount>> cellItem, String componentId, IModel<SsoAccount> rowModel) {
cellItem.add(new Label(componentId, rowModel.getObject().getSubject()));
}
});
columns.add(new AbstractColumn<SsoAccount, Void>(Model.of("")) {
@Override
public void populateItem(Item<ICellPopulator<SsoAccount>> cellItem, String componentId,
IModel<SsoAccount> rowModel) {
Fragment fragment = new Fragment(componentId, "actionFrag", SsoAccountListPanel.this);
fragment.add(new AjaxLink<Void>("delete") {
@Override
public void onClick(AjaxRequestTarget target) {
var ssoAccount = rowModel.getObject();
OneDev.getInstance(SsoAccountManager.class).delete(ssoAccount);
if (getPage() instanceof UserPage)
OneDev.getInstance(AuditManager.class).audit(null, "deleted SSO account \"" + ssoAccount.getProvider() + "/" + ssoAccount.getSubject() + "\" from account \"" + ssoAccount.getUser().getName() + "\"", null, null);
Session.get().success(_T("SSO account deleted"));
target.add(ssoAccountsTable);
}
@Override
protected void updateAjaxAttributes(AjaxRequestAttributes attributes) {
super.updateAjaxAttributes(attributes);
String message = _T("Do you really want to delete this SSO account?");
attributes.getAjaxCallListeners().add(new ConfirmClickListener(message));
}
});
cellItem.add(fragment);
}
@Override
public String getCssClass() {
return "actions";
}
});
SortableDataProvider<SsoAccount, Void> dataProvider = new LoadableDetachableDataProvider<SsoAccount, Void>() {
@Override
public Iterator<? extends SsoAccount> iterator(long first, long count) {
var ssoAccounts = new ArrayList<>(getUser().getSsoAccounts());
Collections.sort(ssoAccounts, new Comparator<SsoAccount>() {
@Override
public int compare(SsoAccount o1, SsoAccount o2) {
return (o1.getProvider().getName() + "/" + o1.getSubject()).compareTo(o2.getProvider().getName() + "/" + o2.getSubject());
}
});
return ssoAccounts.iterator();
}
@Override
public long calcSize() {
return getUser().getSsoAccounts().size();
}
@Override
public IModel<SsoAccount> model(SsoAccount ssoAccount) {
Long id = ssoAccount.getId();
return new LoadableDetachableModel<SsoAccount>() {
@Override
protected SsoAccount load() {
return OneDev.getInstance(SsoAccountManager.class).load(id);
}
};
}
};
add(ssoAccountsTable = new DefaultDataTable<SsoAccount, Void>("ssoAccounts", columns, dataProvider,
Integer.MAX_VALUE, null));
}
}

View File

@ -31,7 +31,7 @@ import io.onedev.server.entitymanager.UserManager;
import io.onedev.server.model.User;
import io.onedev.server.model.support.TwoFactorAuthentication;
import io.onedev.server.util.CryptoUtils;
import io.onedev.server.web.page.simple.security.LoginPage;
import io.onedev.server.web.page.security.LoginPage;
public abstract class TwoFactorAuthenticationSetupPanel extends GenericPanel<User> {

View File

@ -44,8 +44,8 @@ import io.onedev.server.web.behavior.AbstractPostAjaxBehavior;
import io.onedev.server.web.behavior.OnTypingDoneBehavior;
import io.onedev.server.web.editable.PropertyDescriptor;
import io.onedev.server.web.editable.PropertyEditor;
import io.onedev.server.web.page.security.OAuthCallbackPage;
import io.onedev.server.annotation.RefreshToken;
import io.onedev.server.web.page.simple.security.OAuthCallbackPage;
public class RefreshTokenPropertyEditor extends PropertyEditor<String> {
@ -151,7 +151,7 @@ public class RefreshTokenPropertyEditor extends PropertyEditor<String> {
URI tokenEndpoint = new URI(callback.getTokenEndpoint());
TokenRequest request = new TokenRequest(tokenEndpoint, clientAuth, codeGrant);
TokenRequest request = new TokenRequest(tokenEndpoint, clientAuth, codeGrant, null);
TokenResponse response = TokenResponse.parse(request.toHTTPRequest().send());
if (response.indicatesSuccess()) {

View File

@ -60,14 +60,16 @@ import io.onedev.server.web.page.admin.securitysetting.SecuritySettingPage;
import io.onedev.server.web.page.admin.serverinformation.ServerInformationPage;
import io.onedev.server.web.page.admin.serverlog.ServerLogPage;
import io.onedev.server.web.page.admin.sshserverkey.SshServerKeyPage;
import io.onedev.server.web.page.admin.ssosetting.SsoConnectorListPage;
import io.onedev.server.web.page.admin.ssosetting.SsoProcessPage;
import io.onedev.server.web.page.admin.ssosetting.NewSsoProviderPage;
import io.onedev.server.web.page.admin.ssosetting.SsoProviderDetailPage;
import io.onedev.server.web.page.admin.ssosetting.SsoProviderListPage;
import io.onedev.server.web.page.admin.systemsetting.SystemSettingPage;
import io.onedev.server.web.page.admin.usermanagement.InvitationListPage;
import io.onedev.server.web.page.admin.usermanagement.NewInvitationPage;
import io.onedev.server.web.page.admin.usermanagement.NewUserPage;
import io.onedev.server.web.page.admin.usermanagement.UserListPage;
import io.onedev.server.web.page.builds.BuildListPage;
import io.onedev.server.web.page.error.PageNotFoundErrorPage;
import io.onedev.server.web.page.help.IncompatibilitiesPage;
import io.onedev.server.web.page.help.MethodDetailPage;
import io.onedev.server.web.page.help.ResourceDetailPage;
@ -82,6 +84,7 @@ import io.onedev.server.web.page.my.password.MyPasswordPage;
import io.onedev.server.web.page.my.profile.MyProfilePage;
import io.onedev.server.web.page.my.querywatch.MyQueryWatchesPage;
import io.onedev.server.web.page.my.sshkeys.MySshKeysPage;
import io.onedev.server.web.page.my.ssoaccounts.MySsoAccountsPage;
import io.onedev.server.web.page.my.twofactorauthentication.MyTwoFactorAuthenticationPage;
import io.onedev.server.web.page.packs.PackListPage;
import io.onedev.server.web.page.project.NewProjectPage;
@ -148,15 +151,15 @@ import io.onedev.server.web.page.project.stats.code.CodeContribsPage;
import io.onedev.server.web.page.project.stats.code.SourceLinesPage;
import io.onedev.server.web.page.project.tags.ProjectTagsPage;
import io.onedev.server.web.page.pullrequests.PullRequestListPage;
import io.onedev.server.web.page.simple.error.PageNotFoundErrorPage;
import io.onedev.server.web.page.simple.security.CreateUserFromInvitationPage;
import io.onedev.server.web.page.simple.security.EmailAddressVerificationPage;
import io.onedev.server.web.page.simple.security.LoginPage;
import io.onedev.server.web.page.simple.security.LogoutPage;
import io.onedev.server.web.page.simple.security.OAuthCallbackPage;
import io.onedev.server.web.page.simple.security.PasswordResetPage;
import io.onedev.server.web.page.simple.security.SignUpPage;
import io.onedev.server.web.page.simple.serverinit.ServerInitPage;
import io.onedev.server.web.page.security.CreateUserFromInvitationPage;
import io.onedev.server.web.page.security.EmailAddressVerificationPage;
import io.onedev.server.web.page.security.LoginPage;
import io.onedev.server.web.page.security.LogoutPage;
import io.onedev.server.web.page.security.OAuthCallbackPage;
import io.onedev.server.web.page.security.PasswordResetPage;
import io.onedev.server.web.page.security.SignUpPage;
import io.onedev.server.web.page.security.SsoProcessPage;
import io.onedev.server.web.page.serverinit.ServerInitPage;
import io.onedev.server.web.page.test.TestPage;
import io.onedev.server.web.page.user.accesstoken.UserAccessTokensPage;
import io.onedev.server.web.page.user.avatar.UserAvatarPage;
@ -168,6 +171,7 @@ import io.onedev.server.web.page.user.password.UserPasswordPage;
import io.onedev.server.web.page.user.profile.UserProfilePage;
import io.onedev.server.web.page.user.querywatch.UserQueryWatchesPage;
import io.onedev.server.web.page.user.sshkeys.UserSshKeysPage;
import io.onedev.server.web.page.user.ssoaccounts.UserSsoAccountsPage;
import io.onedev.server.web.page.user.twofactorauthentication.UserTwoFactorAuthenticationPage;
import io.onedev.server.web.resource.AgentLibResourceReference;
import io.onedev.server.web.resource.AgentLogResourceReference;
@ -226,6 +230,7 @@ public class BaseUrlMapper extends CompoundRequestMapper {
add(new BasePageMapper("~my/gpg-keys", MyGpgKeysPage.class));
add(new BasePageMapper("~my/access-tokens", MyAccessTokensPage.class));
add(new BasePageMapper("~my/two-factor-authentication", MyTwoFactorAuthenticationPage.class));
add(new BasePageMapper("~my/sso-accounts", MySsoAccountsPage.class));
add(new BasePageMapper("~my/query-watches/#{tab}", MyQueryWatchesPage.class));
}
@ -267,7 +272,7 @@ public class BaseUrlMapper extends CompoundRequestMapper {
EmailAddressVerificationPage.class));
add(new BasePageMapper("~create-user-from-invitation/${emailAddress}/${invitationCode}",
CreateUserFromInvitationPage.class));
add(new BasePageMapper(SsoProcessPage.MOUNT_PATH + "/${stage}/${connector}", SsoProcessPage.class));
add(new BasePageMapper(SsoProcessPage.MOUNT_PATH + "/${stage}/${provider}", SsoProcessPage.class));
add(new BasePageMapper(OAuthCallbackPage.MOUNT_PATH, OAuthCallbackPage.class));
}
@ -284,6 +289,7 @@ public class BaseUrlMapper extends CompoundRequestMapper {
add(new BasePageMapper("~users/${user}/gpg-keys", UserGpgKeysPage.class));
add(new BasePageMapper("~users/${user}/access-tokens", UserAccessTokensPage.class));
add(new BasePageMapper("~users/${user}/two-factor-authentication", UserTwoFactorAuthenticationPage.class));
add(new BasePageMapper("~users/${user}/sso-accounts", UserSsoAccountsPage.class));
add(new BasePageMapper("~users/${user}/query-watches/#{tab}", UserQueryWatchesPage.class));
}
@ -342,7 +348,9 @@ public class BaseUrlMapper extends CompoundRequestMapper {
add(new BasePageMapper("~administration/settings/performance", PerformanceSettingPage.class));
add(new BasePageMapper("~administration/settings/backup", DatabaseBackupPage.class));
add(new BasePageMapper("~administration/settings/authenticator", AuthenticatorPage.class));
add(new BasePageMapper("~administration/settings/sso-connectors", SsoConnectorListPage.class));
add(new BasePageMapper("~administration/settings/sso-providers", SsoProviderListPage.class));
add(new BasePageMapper("~administration/settings/sso-providers/new", NewSsoProviderPage.class));
add(new BasePageMapper("~administration/settings/sso-providers/${provider}", SsoProviderDetailPage.class));
add(new BasePageMapper("~administration/settings/ssh-server-key", SshServerKeyPage.class));
add(new BasePageMapper("~administration/settings/gpg-signing-key", GpgSigningKeyPage.class));
add(new BasePageMapper("~administration/settings/gpg-trusted-keys", GpgTrustedKeysPage.class));

View File

@ -12,7 +12,7 @@ public class AuthenticatorBean implements Serializable {
private Authenticator authenticator;
@Editable(placeholder="No external authenticator")
@Editable(placeholder="No external password authenticator")
public Authenticator getAuthenticator() {
return authenticator;
}

View File

@ -1,6 +1,12 @@
<wicket:extend>
<div class="card">
<div class="card-body">
<div class="alert alert-light alert-notice mb-5">
<wicket:svg href="bulb" class="icon mr-2"></wicket:svg>
<wicket:t>When login via OneDev's built-in form, submitted user credentials can be checked against
authenticator defined here, besides the internal database
</wicket:t>
</div>
<form wicket:id="authenticator" class="authenticator leave-confirm">
<div wicket:id="editor" class="mb-5"></div>
<button wicket:id="save" class="btn btn-primary dirty-aware mr-3"><wicket:t>Save Settings</wicket:t></button>

View File

@ -201,7 +201,7 @@ public class AuthenticatorPage extends AdministrationPage {
@Override
protected Component newTopbarTitle(String componentId) {
return new Label(componentId, _T("External Authenticator"));
return new Label(componentId, _T("External Password Authenticator"));
}
}

View File

@ -14,6 +14,7 @@
<a wicket:id="link"><span wicket:id="label"></span></a>
</wicket:fragment>
<wicket:fragment wicket:id="actionFrag">
<a wicket:id="delete" t:data-tippy-content="Delete this group" class="btn btn-xs btn-icon btn-light btn-hover-danger"><wicket:svg href="trash" class="icon align-middle"></wicket:svg></a>
<a wicket:id="edit" class="btn btn-xs btn-icon btn-light btn-hover-primary mr-1" t:data-tippy-content="Edit"><wicket:svg href="edit" class="icon"></wicket:svg></a>
<a wicket:id="delete" t:data-tippy-content="Delete" class="btn btn-xs btn-icon btn-light btn-hover-danger"><wicket:svg href="trash" class="icon align-middle"></wicket:svg></a>
</wicket:fragment>
</wicket:extend>

View File

@ -85,6 +85,19 @@ public class GroupListPage extends AdministrationPage {
target.add(groupsTable);
}
private WebMarkupContainer newProfileLink(String componentId, Group group) {
return new ActionablePageLink(componentId, GroupProfilePage.class, GroupProfilePage.paramsOf(group)) {
@Override
protected void doBeforeNav(AjaxRequestTarget target) {
String redirectUrlAfterDelete = RequestCycle.get().urlFor(
GroupListPage.class, getPageParameters()).toString();
WebSession.get().setRedirectUrlAfterDelete(Group.class, redirectUrlAfterDelete);
}
};
}
@Override
protected void onInitialize() {
super.onInitialize();
@ -148,17 +161,7 @@ public class GroupListPage extends AdministrationPage {
public void populateItem(Item<ICellPopulator<Group>> cellItem, String componentId, IModel<Group> rowModel) {
Fragment fragment = new Fragment(componentId, "nameFrag", GroupListPage.this);
Group group = rowModel.getObject();
WebMarkupContainer link = new ActionablePageLink("link",
GroupProfilePage.class, GroupProfilePage.paramsOf(group)) {
@Override
protected void doBeforeNav(AjaxRequestTarget target) {
String redirectUrlAfterDelete = RequestCycle.get().urlFor(
GroupListPage.class, getPageParameters()).toString();
WebSession.get().setRedirectUrlAfterDelete(Group.class, redirectUrlAfterDelete);
}
};
var link = newProfileLink("link", group);
link.add(new Label("label", group.getName()));
fragment.add(link);
cellItem.add(fragment);
@ -192,6 +195,7 @@ public class GroupListPage extends AdministrationPage {
public void populateItem(Item<ICellPopulator<Group>> cellItem, String componentId, IModel<Group> rowModel) {
Fragment fragment = new Fragment(componentId, "actionFrag", GroupListPage.this);
fragment.add(newProfileLink("edit", rowModel.getObject()));
fragment.add(new AjaxLink<Void>("delete") {
@Override

View File

@ -10,6 +10,6 @@
</div>
</div>
<wicket:fragment wicket:id="topbarTitleFrag">
<a wicket:id="groups"><wicket:t>Groups</wicket:t></a> <span class="dot"></span> <span class="text-truncate"><wicket:t>Create Group</wicket:t></span>
<a wicket:id="groups"><wicket:t>Groups</wicket:t></a> <span class="dot"></span> <span class="text-truncate"><wicket:t>Create New</wicket:t></span>
</wicket:fragment>
</wicket:extend>

View File

@ -92,7 +92,10 @@ public class GroupProfilePage extends GroupPage {
@Override
public void onClick() {
var oldAuditContent = VersionedXmlDoc.fromBean(getGroup()).toXML();
OneDev.getInstance(GroupManager.class).delete(getGroup());
OneDev.getInstance(AuditManager.class).audit(null, "deleted group \"" + getGroup().getName() + "\"", oldAuditContent, null);
Session.get().success(MessageFormat.format(_T("Group \"{0}\" deleted"), getGroup().getName()));
String redirectUrlAfterDelete = WebSession.get().getRedirectUrlAfterDelete(Group.class);

View File

@ -14,6 +14,7 @@
<a wicket:id="link"><span wicket:id="label"></span></a>
</wicket:fragment>
<wicket:fragment wicket:id="actionFrag">
<a wicket:id="delete" class="btn btn-xs btn-icon btn-light btn-hover-danger" t:data-tippy-content="Delete this role"><wicket:svg href="trash" class="icon align-middle"></wicket:svg></a>
<a wicket:id="edit" class="btn btn-xs btn-icon btn-light btn-hover-primary mr-1" t:data-tippy-content="Edit"><wicket:svg href="edit" class="icon"></wicket:svg></a>
<a wicket:id="delete" class="btn btn-xs btn-icon btn-light btn-hover-danger" t:data-tippy-content="Delete"><wicket:svg href="trash" class="icon align-middle"></wicket:svg></a>
</wicket:fragment>
</wicket:extend>

View File

@ -82,6 +82,20 @@ public class RoleListPage extends AdministrationPage {
target.add(rolesTable);
}
private WebMarkupContainer newDetailLink(String componentId, Role role) {
return new ActionablePageLink(componentId,
RoleDetailPage.class, RoleDetailPage.paramsOf(role)) {
@Override
public void doBeforeNav(AjaxRequestTarget target) {
String redirectUrlAfterDelete = RequestCycle.get().urlFor(
RoleListPage.class, getPageParameters()).toString();
WebSession.get().setRedirectUrlAfterDelete(Role.class, redirectUrlAfterDelete);
}
};
}
@Override
protected void onInitialize() {
super.onInitialize();
@ -152,17 +166,7 @@ public class RoleListPage extends AdministrationPage {
Role role = rowModel.getObject();
WebMarkupContainer link = new ActionablePageLink("link",
RoleDetailPage.class, RoleDetailPage.paramsOf(role)) {
@Override
public void doBeforeNav(AjaxRequestTarget target) {
String redirectUrlAfterDelete = RequestCycle.get().urlFor(
RoleListPage.class, getPageParameters()).toString();
WebSession.get().setRedirectUrlAfterDelete(Role.class, redirectUrlAfterDelete);
}
};
WebMarkupContainer link = newDetailLink("link", role);
link.add(new Label("label", role.getName()));
fragment.add(link);
@ -186,7 +190,9 @@ public class RoleListPage extends AdministrationPage {
@Override
public void populateItem(Item<ICellPopulator<Role>> cellItem, String componentId, IModel<Role> rowModel) {
Fragment fragment = new Fragment(componentId, "actionFrag", RoleListPage.this);
fragment.add(newDetailLink("edit", rowModel.getObject()));
fragment.add(new AjaxLink<Void>("delete") {
@Override

View File

@ -0,0 +1,15 @@
<wicket:extend>
<div class="sso-provider-edit card">
<div class="card-body">
<form wicket:id="form" class="leave-confirm">
<div wicket:id="editor"></div>
<div class="actions clearfix">
<input class="btn btn-primary dirty-aware float-left" type="submit" t:value="Save">
</div>
</form>
</div>
</div>
<wicket:fragment wicket:id="topbarTitleFrag">
<a wicket:id="ssoProviders"><wicket:t>SSO Providers</wicket:t></a> <span class="dot"></span> <wicket:t>Create New</wicket:t>
</wicket:fragment>
</wicket:extend>

View File

@ -0,0 +1,89 @@
package io.onedev.server.web.page.admin.ssosetting;
import static io.onedev.server.web.translation.Translation._T;
import org.apache.wicket.Component;
import org.apache.wicket.Session;
import org.apache.wicket.markup.head.CssHeaderItem;
import org.apache.wicket.markup.head.IHeaderResponse;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.link.BookmarkablePageLink;
import org.apache.wicket.markup.html.panel.Fragment;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import io.onedev.server.OneDev;
import io.onedev.server.data.migration.VersionedXmlDoc;
import io.onedev.server.entitymanager.AuditManager;
import io.onedev.server.entitymanager.SsoProviderManager;
import io.onedev.server.model.SsoProvider;
import io.onedev.server.security.SecurityUtils;
import io.onedev.server.util.Path;
import io.onedev.server.util.PathNode;
import io.onedev.server.web.editable.BeanContext;
import io.onedev.server.web.editable.BeanEditor;
import io.onedev.server.web.page.admin.AdministrationPage;
import io.onedev.server.web.page.admin.groupmanagement.GroupCssResourceReference;
public class NewSsoProviderPage extends AdministrationPage {
public NewSsoProviderPage(PageParameters params) {
super(params);
}
@Override
protected void onInitialize() {
super.onInitialize();
var bean = new SsoProviderBean();
BeanEditor editor = BeanContext.edit("editor", bean);
Form<?> form = new Form<Void>("form") {
@Override
protected void onSubmit() {
super.onSubmit();
SsoProvider providerWithSameName = getSsoProviderManager().find(bean.getName());
if (providerWithSameName != null) {
editor.error(new Path(new PathNode.Named("name")),
_T("This name has already been used by another provider"));
}
if (editor.isValid()) {
var provider = new SsoProvider();
bean.populate(provider);
getSsoProviderManager().createOrUpdate(provider);
var newAuditContent = VersionedXmlDoc.fromBean(provider).toXML();
OneDev.getInstance(AuditManager.class).audit(null, "created SSO provider \"" + provider.getName() + "\"", null, newAuditContent);
Session.get().success(_T("SSO provider created"));
setResponsePage(SsoProviderListPage.class);
}
}
};
form.add(editor);
add(form);
}
private SsoProviderManager getSsoProviderManager() {
return OneDev.getInstance(SsoProviderManager.class);
}
@Override
protected boolean isPermitted() {
return SecurityUtils.isAdministrator();
}
@Override
public void renderHead(IHeaderResponse response) {
super.renderHead(response);
response.render(CssHeaderItem.forReference(new GroupCssResourceReference()));
}
@Override
protected Component newTopbarTitle(String componentId) {
Fragment fragment = new Fragment(componentId, "topbarTitleFrag", this);
fragment.add(new BookmarkablePageLink<Void>("ssoProviders", SsoProviderListPage.class));
return fragment;
}
}

View File

@ -1,29 +0,0 @@
package io.onedev.server.web.page.admin.ssosetting;
import java.io.Serializable;
import javax.validation.constraints.NotNull;
import io.onedev.server.model.support.administration.sso.SsoConnector;
import io.onedev.server.annotation.Editable;
import io.onedev.server.annotation.OmitName;
@Editable
public class SsoConnectorBean implements Serializable {
private static final long serialVersionUID = 1L;
private SsoConnector connector;
@Editable
@OmitName
@NotNull
public SsoConnector getConnector() {
return connector;
}
public void setConnector(SsoConnector connector) {
this.connector = connector;
}
}

View File

@ -1,15 +0,0 @@
<wicket:panel>
<form wicket:id="form" class="sso-connector-edit leave-confirm">
<div class="modal-header">
<h5 id="modal-title" class="modal-title"><wicket:t>Single Sign On Provider</wicket:t></h5>
<button wicket:id="close" type="button" class="close"><wicket:svg href="times" class="icon"/></button>
</div>
<div class="modal-body">
<div wicket:id="editor"></div>
</div>
<div class="modal-footer">
<input wicket:id="save" type="submit" class="dirty-aware btn btn-primary" t:value="Save">
<a wicket:id="cancel" class="btn btn-secondary"><wicket:t>Cancel</wicket:t></a>
</div>
</form>
</wicket:panel>

View File

@ -1,155 +0,0 @@
package io.onedev.server.web.page.admin.ssosetting;
import static io.onedev.server.web.translation.Translation._T;
import java.util.List;
import javax.annotation.Nullable;
import org.apache.commons.lang3.SerializationUtils;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.attributes.AjaxRequestAttributes;
import org.apache.wicket.ajax.markup.html.AjaxLink;
import org.apache.wicket.ajax.markup.html.form.AjaxButton;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.panel.Panel;
import org.apache.wicket.request.cycle.RequestCycle;
import io.onedev.server.OneDev;
import io.onedev.server.data.migration.VersionedXmlDoc;
import io.onedev.server.entitymanager.AuditManager;
import io.onedev.server.entitymanager.SettingManager;
import io.onedev.server.model.support.administration.sso.SsoConnector;
import io.onedev.server.persistence.TransactionManager;
import io.onedev.server.util.Path;
import io.onedev.server.util.PathNode;
import io.onedev.server.web.ajaxlistener.ConfirmLeaveListener;
import io.onedev.server.web.editable.BeanContext;
import io.onedev.server.web.editable.BeanEditor;
abstract class SsoConnectorEditPanel extends Panel {
private final int connectorIndex;
public SsoConnectorEditPanel(String id, int connectorIndex) {
super(id);
this.connectorIndex = connectorIndex;
}
@Override
protected void onInitialize() {
super.onInitialize();
SsoConnectorBean bean = new SsoConnectorBean();
if (connectorIndex != -1)
bean.setConnector(SerializationUtils.clone(getConnectors().get(connectorIndex)));
Form<?> form = new Form<Void>("form") {
@Override
protected void onError() {
super.onError();
RequestCycle.get().find(AjaxRequestTarget.class).add(this);
}
};
form.add(new AjaxLink<Void>("close") {
@Override
protected void updateAjaxAttributes(AjaxRequestAttributes attributes) {
super.updateAjaxAttributes(attributes);
attributes.getAjaxCallListeners().add(new ConfirmLeaveListener(SsoConnectorEditPanel.this));
}
@Override
public void onClick(AjaxRequestTarget target) {
onCancel(target);
}
});
BeanEditor editor = BeanContext.edit("editor", bean);
form.add(editor);
form.add(new AjaxButton("save") {
@Override
protected void onSubmit(AjaxRequestTarget target, Form<?> form) {
super.onSubmit(target, form);
if (connectorIndex != -1) {
SsoConnector oldConnector = getConnectors().get(connectorIndex);
if (!bean.getConnector().getName().equals(oldConnector.getName())
&& getConnector(bean.getConnector().getName()) != null) {
editor.error(new Path(new PathNode.Named("connector"), new PathNode.Named("name")),
_T("This name has already been used by another provider"));
}
} else if (getConnector(bean.getConnector().getName()) != null) {
editor.error(new Path(new PathNode.Named("connector"), new PathNode.Named("name")),
_T("This name has already been used by another provider"));
}
if (editor.isValid()) {
OneDev.getInstance(TransactionManager.class).run(new Runnable() {
@Override
public void run() {
var auditManager = OneDev.getInstance(AuditManager.class);
if (connectorIndex != -1) {
var oldConnector = getConnectors().set(connectorIndex, bean.getConnector());
var oldAuditContent = VersionedXmlDoc.fromBean(oldConnector).toXML();
var newAuditContent = VersionedXmlDoc.fromBean(bean.getConnector()).toXML();
auditManager.audit(null, "changed sso connector \"" + bean.getConnector().getName() + "\"", oldAuditContent, newAuditContent);
} else {
getConnectors().add(bean.getConnector());
var newAuditContent = VersionedXmlDoc.fromBean(bean.getConnector()).toXML();
auditManager.audit(null, "created sso connector \"" + bean.getConnector().getName() + "\"", null, newAuditContent);
}
OneDev.getInstance(SettingManager.class).saveSsoConnectors(getConnectors());
}
});
onSave(target);
} else {
target.add(form);
}
}
});
form.add(new AjaxLink<Void>("cancel") {
@Override
protected void updateAjaxAttributes(AjaxRequestAttributes attributes) {
super.updateAjaxAttributes(attributes);
attributes.getAjaxCallListeners().add(new ConfirmLeaveListener(SsoConnectorEditPanel.this));
}
@Override
public void onClick(AjaxRequestTarget target) {
onCancel(target);
}
});
form.setOutputMarkupId(true);
add(form);
}
@Nullable
private SsoConnector getConnector(String name) {
for (SsoConnector connector: getConnectors()) {
if (connector.getName().equals(name))
return connector;
}
return null;
}
protected abstract List<SsoConnector> getConnectors();
protected abstract void onSave(AjaxRequestTarget target);
protected abstract void onCancel(AjaxRequestTarget target);
}

View File

@ -1,243 +0,0 @@
package io.onedev.server.web.page.admin.ssosetting;
import static io.onedev.server.web.translation.Translation._T;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.apache.wicket.Component;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.attributes.AjaxRequestAttributes;
import org.apache.wicket.ajax.markup.html.AjaxLink;
import org.apache.wicket.behavior.AttributeAppender;
import org.apache.wicket.extensions.markup.html.repeater.data.grid.ICellPopulator;
import org.apache.wicket.extensions.markup.html.repeater.data.table.AbstractColumn;
import org.apache.wicket.extensions.markup.html.repeater.data.table.DataTable;
import org.apache.wicket.extensions.markup.html.repeater.data.table.HeadersToolbar;
import org.apache.wicket.extensions.markup.html.repeater.data.table.IColumn;
import org.apache.wicket.extensions.markup.html.repeater.data.table.NoRecordsToolbar;
import org.apache.wicket.markup.ComponentTag;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.list.LoopItem;
import org.apache.wicket.markup.html.panel.Fragment;
import org.apache.wicket.markup.repeater.Item;
import org.apache.wicket.markup.repeater.data.IDataProvider;
import org.apache.wicket.markup.repeater.data.ListDataProvider;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import io.onedev.server.OneDev;
import io.onedev.server.data.migration.VersionedXmlDoc;
import io.onedev.server.entitymanager.AuditManager;
import io.onedev.server.entitymanager.SettingManager;
import io.onedev.server.model.support.administration.sso.SsoConnector;
import io.onedev.server.persistence.TransactionManager;
import io.onedev.server.web.ajaxlistener.ConfirmClickListener;
import io.onedev.server.web.behavior.NoRecordsBehavior;
import io.onedev.server.web.behavior.sortable.SortBehavior;
import io.onedev.server.web.behavior.sortable.SortPosition;
import io.onedev.server.web.component.link.copytoclipboard.CopyToClipboardLink;
import io.onedev.server.web.component.modal.ModalLink;
import io.onedev.server.web.component.modal.ModalPanel;
import io.onedev.server.web.component.svg.SpriteImage;
import io.onedev.server.web.page.admin.AdministrationPage;
public class SsoConnectorListPage extends AdministrationPage {
private final List<SsoConnector> connectors;
public SsoConnectorListPage(PageParameters params) {
super(params);
connectors = OneDev.getInstance(SettingManager.class).getSsoConnectors();
}
private DataTable<SsoConnector, Void> connectorsTable;
@Override
protected void onInitialize() {
super.onInitialize();
add(new ModalLink("addNew") {
@Override
protected Component newContent(String id, ModalPanel modal) {
return new SsoConnectorEditPanel(id, -1) {
@Override
protected void onSave(AjaxRequestTarget target) {
target.add(connectorsTable);
modal.close();
}
@Override
protected void onCancel(AjaxRequestTarget target) {
modal.close();
}
@Override
protected List<SsoConnector> getConnectors() {
return connectors;
}
};
}
});
List<IColumn<SsoConnector, Void>> columns = new ArrayList<>();
columns.add(new AbstractColumn<SsoConnector, Void>(Model.of("")) {
@Override
public void populateItem(Item<ICellPopulator<SsoConnector>> cellItem, String componentId, IModel<SsoConnector> rowModel) {
cellItem.add(new SpriteImage(componentId, "grip") {
@Override
protected void onComponentTag(ComponentTag tag) {
super.onComponentTag(tag);
tag.setName("svg");
tag.put("class", "icon drag-indicator");
}
});
}
@Override
public String getCssClass() {
return "minimum actions";
}
});
columns.add(new AbstractColumn<SsoConnector, Void>(Model.of(_T("Name"))) {
@Override
public void populateItem(Item<ICellPopulator<SsoConnector>> cellItem, String componentId, IModel<SsoConnector> rowModel) {
cellItem.add(new Label(componentId, rowModel.getObject().getName()));
}
});
columns.add(new AbstractColumn<SsoConnector, Void>(Model.of(_T("Callback URL"))) {
@Override
public void populateItem(Item<ICellPopulator<SsoConnector>> cellItem, String componentId, IModel<SsoConnector> rowModel) {
SsoConnector connector = rowModel.getObject();
Fragment fragment = new Fragment(componentId, "callbackUriFrag", SsoConnectorListPage.this);
fragment.add(new Label("value", connector.getCallbackUri().toString()));
fragment.add(new CopyToClipboardLink("copy", Model.of(connector.getCallbackUri().toString())));
cellItem.add(fragment);
}
});
columns.add(new AbstractColumn<SsoConnector, Void>(Model.of("")) {
@Override
public void populateItem(Item<ICellPopulator<SsoConnector>> cellItem, String componentId, IModel<SsoConnector> rowModel) {
int connectorIndex = cellItem.findParent(LoopItem.class).getIndex();
Fragment fragment = new Fragment(componentId, "actionColumnFrag", SsoConnectorListPage.this);
fragment.add(AttributeAppender.append("class", "text-nowrap"));
fragment.add(new ModalLink("edit") {
@Override
protected Component newContent(String id, ModalPanel modal) {
return new SsoConnectorEditPanel(id, connectorIndex) {
@Override
protected void onSave(AjaxRequestTarget target) {
target.add(connectorsTable);
modal.close();
}
@Override
protected void onCancel(AjaxRequestTarget target) {
modal.close();
}
@Override
protected List<SsoConnector> getConnectors() {
return connectors;
}
};
}
});
fragment.add(new AjaxLink<Void>("delete") {
@Override
protected void updateAjaxAttributes(AjaxRequestAttributes attributes) {
super.updateAjaxAttributes(attributes);
attributes.getAjaxCallListeners().add(new ConfirmClickListener(_T("Do you really want to delete this connector?")));
}
@Override
public void onClick(AjaxRequestTarget target) {
var connector = connectors.remove(connectorIndex);
OneDev.getInstance(TransactionManager.class).run(new Runnable() {
@Override
public void run() {
OneDev.getInstance(SettingManager.class).saveSsoConnectors(connectors);
var oldAuditContent = VersionedXmlDoc.fromBean(connector).toXML();
OneDev.getInstance(AuditManager.class).audit(null, "deleted sso connector \"" + connector.getName() + "\"", oldAuditContent, null);
}
});
target.add(connectorsTable);
}
});
cellItem.add(fragment);
}
@Override
public String getCssClass() {
return "actions";
}
});
IDataProvider<SsoConnector> dataProvider = new ListDataProvider<SsoConnector>() {
@Override
protected List<SsoConnector> getData() {
return connectors;
}
};
add(connectorsTable = new DataTable<SsoConnector, Void>("connectors", columns, dataProvider, Integer.MAX_VALUE));
connectorsTable.addTopToolbar(new HeadersToolbar<Void>(connectorsTable, null));
connectorsTable.addBottomToolbar(new NoRecordsToolbar(connectorsTable));
connectorsTable.add(new NoRecordsBehavior());
connectorsTable.setOutputMarkupId(true);
connectorsTable.add(new SortBehavior() {
@Override
protected void onSort(AjaxRequestTarget target, SortPosition from, SortPosition to) {
int fromIndex = from.getItemIndex();
int toIndex = to.getItemIndex();
if (fromIndex < toIndex) {
for (int i=0; i<toIndex-fromIndex; i++)
Collections.swap(connectors, fromIndex+i, fromIndex+i+1);
} else {
for (int i=0; i<fromIndex-toIndex; i++)
Collections.swap(connectors, fromIndex-i, fromIndex-i-1);
}
OneDev.getInstance(SettingManager.class).saveSsoConnectors(connectors);
target.add(connectorsTable);
}
}.sortable("tbody"));
}
@Override
protected Component newTopbarTitle(String componentId) {
return new Label(componentId, "<span class='text-truncate'>" + _T("Single Sign On") + "</span>").setEscapeModelStrings(false);
}
}

View File

@ -1,117 +0,0 @@
package io.onedev.server.web.page.admin.ssosetting;
import static io.onedev.server.web.translation.Translation._T;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.wicket.RestartResponseAtInterceptPageException;
import org.apache.wicket.RestartResponseException;
import org.apache.wicket.Session;
import org.apache.wicket.request.Url;
import org.apache.wicket.request.cycle.RequestCycle;
import org.apache.wicket.request.flow.RedirectToUrlException;
import org.apache.wicket.request.http.WebRequest;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import io.onedev.commons.utils.StringUtils;
import io.onedev.server.OneDev;
import io.onedev.server.entitymanager.SettingManager;
import io.onedev.server.model.support.administration.sso.SsoAuthenticated;
import io.onedev.server.model.support.administration.sso.SsoConnector;
import io.onedev.server.web.WebSession;
import io.onedev.server.web.page.HomePage;
import io.onedev.server.web.page.base.BasePage;
import io.onedev.server.web.page.simple.security.LoginPage;
public class SsoProcessPage extends BasePage {
public static final String MOUNT_PATH = "~sso";
public static final String COOKIE_CONNECTOR = "ssoConnector";
public static final String STAGE_INITIATE = "initiate";
public static final String STAGE_CALLBACK = "callback";
private static final String PARAM_CONNECTOR = "connector";
private static final String PARAM_STAGE = "stage";
private static final String SESSION_ATTR_REDIRECT_URL = "redirectUrl";
private final SsoConnector connector;
private final String stage;
public SsoProcessPage(PageParameters params) {
super(params);
stage = params.get(PARAM_STAGE).toString();
try {
String connectorName = params.get(PARAM_CONNECTOR).toString();
connector = OneDev.getInstance(SettingManager.class).getSsoConnectors().stream()
.filter(it->it.getName().equals(connectorName))
.findFirst()
.orElse(null);
if (connector == null)
throw new AuthenticationException(_T("Unable to find SSO connector: ") + connectorName);
if (stage.equals(STAGE_INITIATE)) {
String redirectUrlAfterLogin;
Url url = RestartResponseAtInterceptPageException.getOriginalUrl();
if (url != null && url.toString().length() != 0)
redirectUrlAfterLogin = url.toString();
else
redirectUrlAfterLogin = RequestCycle.get().urlFor(HomePage.class, new PageParameters()).toString();
Session.get().bind();
Session.get().setAttribute(SESSION_ATTR_REDIRECT_URL, redirectUrlAfterLogin);
connector.initiateLogin();
} else {
SsoAuthenticated authenticated = connector.processLoginResponse();
String redirectUrlAfterLogin = (String) Session.get().getAttribute(SESSION_ATTR_REDIRECT_URL);
if (StringUtils.isBlank(redirectUrlAfterLogin))
throw new AuthenticationException(_T("Unsolicited OIDC authentication response"));
WebSession.get().login(authenticated);
// Use servlet api to set cookie which will work even if page is redirected
HttpServletResponse response = (HttpServletResponse) RequestCycle.get().getResponse().getContainerResponse();
Cookie cookie = new Cookie(SsoProcessPage.COOKIE_CONNECTOR, connectorName);
cookie.setMaxAge(Integer.MAX_VALUE);
cookie.setPath("/");
response.addCookie(cookie);
throw new RedirectToUrlException(redirectUrlAfterLogin);
}
} catch (AuthenticationException e) {
throw new RestartResponseException(new LoginPage(e.getMessage()));
}
}
public static Cookie getConnectorCookie() {
return ((WebRequest) RequestCycle.get().getRequest()).getCookie(COOKIE_CONNECTOR);
}
public static void clearConnectorCookie() {
// Use servlet api to clear cookie which will work even if page is redirected
HttpServletResponse response = (HttpServletResponse) RequestCycle.get().getResponse().getContainerResponse();
Cookie cookie = new Cookie(COOKIE_CONNECTOR, "");
cookie.setMaxAge(0);
cookie.setPath("/");
response.addCookie(cookie);
}
public static PageParameters paramsOf(String stage, String connector) {
PageParameters params = new PageParameters();
params.add(PARAM_STAGE, stage);
params.add(PARAM_CONNECTOR, connector);
return params;
}
}

View File

@ -0,0 +1,78 @@
package io.onedev.server.web.page.admin.ssosetting;
import java.io.Serializable;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import io.onedev.server.OneDev;
import io.onedev.server.annotation.Editable;
import io.onedev.server.annotation.GroupChoice;
import io.onedev.server.annotation.UrlSegment;
import io.onedev.server.entitymanager.GroupManager;
import io.onedev.server.model.SsoProvider;
import io.onedev.server.model.support.administration.sso.SsoConnector;
import io.onedev.server.web.page.security.SsoProcessPage;
@Editable
public class SsoProviderBean implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private String defaultGroup;
private SsoConnector connector;
@Editable(order=100, description="Name of the provider will serve two purpose: "
+ "<ul>"
+ "<li>Display on login button"
+ "<li>Form the authorization callback url which will be <i>&lt;server url&gt;/" + SsoProcessPage.MOUNT_PATH + "/" + SsoProcessPage.STAGE_CALLBACK + "/&lt;name&gt;</i>"
+ "</ul>")
@UrlSegment // will be used as part of callback url
@NotEmpty
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Editable(order=200, name="Type")
@NotNull
public SsoConnector getConnector() {
return connector;
}
public void setConnector(SsoConnector connector) {
this.connector = connector;
}
@Editable(order=300, placeholder="No default group", description="Optionally add newly authenticated "
+ "user to specified group if membership information is not available")
@GroupChoice
public String getDefaultGroup() {
return defaultGroup;
}
public void setDefaultGroup(String defaultGroup) {
this.defaultGroup = defaultGroup;
}
public void populate(SsoProvider provider) {
provider.setName(name);
provider.setConnector(connector);
if (defaultGroup != null)
provider.setDefaultGroup(OneDev.getInstance(GroupManager.class).find(defaultGroup));
}
public static SsoProviderBean of(SsoProvider provider) {
SsoProviderBean bean = new SsoProviderBean();
bean.name = provider.getName();
bean.connector = provider.getConnector();
bean.defaultGroup = provider.getDefaultGroup() != null ? provider.getDefaultGroup().getName() : null;
return bean;
}
}

View File

@ -0,0 +1,16 @@
<wicket:extend>
<div class="sso-provider-edit card">
<div class="card-body">
<form wicket:id="form" class="leave-confirm">
<div wicket:id="editor"></div>
<div class="actions clearfix">
<input class="btn btn-primary dirty-aware float-left" type="submit" t:value="Save">
<a wicket:id="delete" class="btn btn-light-danger float-right"><wicket:t>Delete</wicket:t></a>
</div>
</form>
</div>
</div>
<wicket:fragment wicket:id="topbarTitleFrag">
<a wicket:id="ssoProviders"><wicket:t>SSO Providers</wicket:t></a> <span class="dot"></span> <span class="text-truncate"><span wicket:id="providerName"></span></span>
</wicket:fragment>
</wicket:extend>

View File

@ -0,0 +1,141 @@
package io.onedev.server.web.page.admin.ssosetting;
import static io.onedev.server.web.translation.Translation._T;
import java.text.MessageFormat;
import org.apache.wicket.Component;
import org.apache.wicket.RestartResponseException;
import org.apache.wicket.Session;
import org.apache.wicket.markup.head.CssHeaderItem;
import org.apache.wicket.markup.head.IHeaderResponse;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.link.BookmarkablePageLink;
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.apache.wicket.request.mapper.parameter.PageParameters;
import io.onedev.server.OneDev;
import io.onedev.server.data.migration.VersionedXmlDoc;
import io.onedev.server.entitymanager.AuditManager;
import io.onedev.server.entitymanager.SsoProviderManager;
import io.onedev.server.model.SsoProvider;
import io.onedev.server.security.SecurityUtils;
import io.onedev.server.util.Path;
import io.onedev.server.util.PathNode;
import io.onedev.server.web.editable.BeanContext;
import io.onedev.server.web.editable.BeanEditor;
import io.onedev.server.web.page.admin.AdministrationPage;
import io.onedev.server.web.page.admin.groupmanagement.GroupCssResourceReference;
import io.onedev.server.web.util.ConfirmClickModifier;
public class SsoProviderDetailPage extends AdministrationPage {
private static final String PARAM_PROVIDER = "provider";
private final IModel<SsoProvider> providerModel;
public SsoProviderDetailPage(PageParameters params) {
super(params);
Long providerId = params.get(PARAM_PROVIDER).toOptionalLong();
if (providerId == null)
throw new RestartResponseException(SsoProviderListPage.class);
providerModel = new LoadableDetachableModel<SsoProvider>() {
@Override
protected SsoProvider load() {
return OneDev.getInstance(SsoProviderManager.class).load(providerId);
}
};
}
@Override
protected void onInitialize() {
super.onInitialize();
var bean = SsoProviderBean.of(providerModel.getObject());
BeanEditor editor = BeanContext.edit("editor", bean);
Form<?> form = new Form<Void>("form") {
@Override
protected void onSubmit() {
super.onSubmit();
SsoProvider providerWithSameName = getSsoProviderManager().find(bean.getName());
if (providerWithSameName != null && !providerWithSameName.equals(providerModel.getObject())) {
editor.error(new Path(new PathNode.Named("name")),
_T("This name has already been used by another provider"));
}
if (editor.isValid()) {
var provider = providerModel.getObject();
var oldAuditContent = VersionedXmlDoc.fromBean(provider).toXML();
bean.populate(provider);
getSsoProviderManager().createOrUpdate(provider);
var newAuditContent = VersionedXmlDoc.fromBean(provider).toXML();
OneDev.getInstance(AuditManager.class).audit(null, "changed sso provider \"" + provider.getName() + "\"", oldAuditContent, newAuditContent);
Session.get().success(_T("SSO provider updated"));
setResponsePage(SsoProviderListPage.class);
}
}
};
form.add(editor);
form.add(new Link<Void>("delete") {
@Override
public void onClick() {
var oldAuditContent = VersionedXmlDoc.fromBean(providerModel.getObject()).toXML();
getSsoProviderManager().delete(providerModel.getObject());
OneDev.getInstance(AuditManager.class).audit(null, "deleted SSO provider \"" + providerModel.getObject().getName() + "\"", oldAuditContent, null);
Session.get().success(MessageFormat.format(_T("SSO provider \"{0}\" deleted"), providerModel.getObject().getName()));
setResponsePage(SsoProviderListPage.class);
}
}.add(new ConfirmClickModifier(MessageFormat.format(_T("Do you really want to delete SSO provider \"{0}\"?"), providerModel.getObject().getName()))));
add(form);
}
@Override
protected boolean isPermitted() {
return SecurityUtils.isAdministrator();
}
@Override
public void renderHead(IHeaderResponse response) {
super.renderHead(response);
response.render(CssHeaderItem.forReference(new GroupCssResourceReference()));
}
private SsoProviderManager getSsoProviderManager() {
return OneDev.getInstance(SsoProviderManager.class);
}
@Override
protected void onDetach() {
providerModel.detach();
super.onDetach();
}
@Override
protected Component newTopbarTitle(String componentId) {
Fragment fragment = new Fragment(componentId, "topbarTitleFrag", this);
fragment.add(new BookmarkablePageLink<Void>("ssoProviders", SsoProviderListPage.class));
fragment.add(new Label("providerName", providerModel.getObject().getName()));
return fragment;
}
public static PageParameters paramsOf(SsoProvider ssoProvider) {
var params = new PageParameters();
params.add(PARAM_PROVIDER, ssoProvider.getId());
return params;
}
}

View File

@ -2,9 +2,12 @@
<div class="card">
<div class="card-body">
<a wicket:id="addNew" class="btn btn-icon btn-primary mb-4" t:data-tippy-content="Add SSO provider"><wicket:svg href="plus" class="icon"/></a>
<table wicket:id="connectors" class="table table-hover"></table>
<table wicket:id="providers" class="table table-hover"></table>
</div>
</div>
<wicket:fragment wicket:id="nameFrag">
<a wicket:id="link"><span wicket:id="label"></span></a>
</wicket:fragment>
<wicket:fragment wicket:id="actionColumnFrag">
<a wicket:id="edit" class="btn btn-xs btn-icon btn-light btn-hover-primary mr-1" t:data-tippy-content="Edit"><wicket:svg href="edit" class="icon"></wicket:svg></a>
<a wicket:id="delete" class="btn btn-xs btn-icon btn-light btn-hover-danger" t:data-tippy-content="Delete"><wicket:svg href="trash" class="icon"></wicket:svg></a>

View File

@ -0,0 +1,158 @@
package io.onedev.server.web.page.admin.ssosetting;
import static io.onedev.server.web.translation.Translation._T;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.apache.wicket.Component;
import org.apache.wicket.Session;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.attributes.AjaxRequestAttributes;
import org.apache.wicket.ajax.markup.html.AjaxLink;
import org.apache.wicket.behavior.AttributeAppender;
import org.apache.wicket.extensions.markup.html.repeater.data.grid.ICellPopulator;
import org.apache.wicket.extensions.markup.html.repeater.data.table.AbstractColumn;
import org.apache.wicket.extensions.markup.html.repeater.data.table.DataTable;
import org.apache.wicket.extensions.markup.html.repeater.data.table.IColumn;
import org.apache.wicket.extensions.markup.html.repeater.util.SortableDataProvider;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.link.BookmarkablePageLink;
import org.apache.wicket.markup.html.panel.Fragment;
import org.apache.wicket.markup.repeater.Item;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.LoadableDetachableModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import io.onedev.server.OneDev;
import io.onedev.server.data.migration.VersionedXmlDoc;
import io.onedev.server.entitymanager.AuditManager;
import io.onedev.server.entitymanager.SsoProviderManager;
import io.onedev.server.model.SsoProvider;
import io.onedev.server.web.ajaxlistener.ConfirmClickListener;
import io.onedev.server.web.component.datatable.DefaultDataTable;
import io.onedev.server.web.component.link.copytoclipboard.CopyToClipboardLink;
import io.onedev.server.web.page.admin.AdministrationPage;
public class SsoProviderListPage extends AdministrationPage {
private DataTable<SsoProvider, Void> providersTable;
public SsoProviderListPage(PageParameters params) {
super(params);
}
@Override
protected void onInitialize() {
super.onInitialize();
add(new BookmarkablePageLink<Void>("addNew", NewSsoProviderPage.class));
List<IColumn<SsoProvider, Void>> columns = new ArrayList<>();
columns.add(new AbstractColumn<SsoProvider, Void>(Model.of(_T("Name"))) {
@Override
public void populateItem(Item<ICellPopulator<SsoProvider>> cellItem, String componentId, IModel<SsoProvider> rowModel) {
Fragment fragment = new Fragment(componentId, "nameFrag", SsoProviderListPage.this);
var link = new BookmarkablePageLink<Void>("link", SsoProviderDetailPage.class,
SsoProviderDetailPage.paramsOf(rowModel.getObject()));
link.add(new Label("label", rowModel.getObject().getName()));
fragment.add(link);
cellItem.add(fragment);
}
});
columns.add(new AbstractColumn<SsoProvider, Void>(Model.of(_T("Callback URL"))) {
@Override
public void populateItem(Item<ICellPopulator<SsoProvider>> cellItem, String componentId, IModel<SsoProvider> rowModel) {
SsoProvider provider = rowModel.getObject();
Fragment fragment = new Fragment(componentId, "callbackUriFrag", SsoProviderListPage.this);
fragment.add(new Label("value", provider.getConnector().getCallbackUri(provider.getName()).toString()));
fragment.add(new CopyToClipboardLink("copy", Model.of(provider.getConnector().getCallbackUri(provider.getName()).toString())));
cellItem.add(fragment);
}
});
columns.add(new AbstractColumn<SsoProvider, Void>(Model.of("")) {
@Override
public void populateItem(Item<ICellPopulator<SsoProvider>> cellItem, String componentId, IModel<SsoProvider> rowModel) {
Fragment fragment = new Fragment(componentId, "actionColumnFrag", SsoProviderListPage.this);
fragment.add(AttributeAppender.append("class", "text-nowrap"));
fragment.add(new BookmarkablePageLink<>("edit", SsoProviderDetailPage.class, SsoProviderDetailPage.paramsOf(rowModel.getObject())));
fragment.add(new AjaxLink<Void>("delete") {
@Override
protected void updateAjaxAttributes(AjaxRequestAttributes attributes) {
super.updateAjaxAttributes(attributes);
String message = MessageFormat.format(_T("Do you really want to delete SSO provider \"{0}\"?"), rowModel.getObject().getName());
attributes.getAjaxCallListeners().add(new ConfirmClickListener(message));
}
@Override
public void onClick(AjaxRequestTarget target) {
SsoProvider provider = rowModel.getObject();
var oldAuditContent = VersionedXmlDoc.fromBean(provider).toXML();
getSsoProviderManager().delete(provider);
OneDev.getInstance(AuditManager.class).audit(null, "deleted SSO provider \"" + provider.getName() + "\"", oldAuditContent, null);
Session.get().success(MessageFormat.format(_T("SSO provider \"{0}\" deleted"), provider.getName()));
target.add(providersTable);
}
});
cellItem.add(fragment);
}
@Override
public String getCssClass() {
return "actions";
}
});
SortableDataProvider<SsoProvider, Void> dataProvider = new SortableDataProvider<>() {
@Override
public Iterator<? extends SsoProvider> iterator(long first, long count) {
return getSsoProviderManager().query().iterator();
}
@Override
public long size() {
return getSsoProviderManager().count();
}
@Override
public IModel<SsoProvider> model(SsoProvider object) {
Long id = object.getId();
return new LoadableDetachableModel<>() {
@Override
protected SsoProvider load() {
return getSsoProviderManager().load(id);
}
};
}
};
add(providersTable = new DefaultDataTable<>("providers", columns, dataProvider, Integer.MAX_VALUE, null));
providersTable.setOutputMarkupId(true);
}
private SsoProviderManager getSsoProviderManager() {
return OneDev.getInstance(SsoProviderManager.class);
}
@Override
protected Component newTopbarTitle(String componentId) {
return new Label(componentId, "<span class='text-truncate'>" + _T("SSO Providers") + "</span>").setEscapeModelStrings(false);
}
}

View File

@ -29,7 +29,8 @@
</a>
</wicket:fragment>
<wicket:fragment wicket:id="actionsFrag">
<a wicket:id="impersonate" t:data-tippy-content="Impersonate this user" class="btn btn-xs btn-icon btn-light btn-hover-primary mr-1"><wicket:svg href="user-tick" class="icon align-middle"/></a>
<a wicket:id="edit" class="btn btn-xs btn-icon btn-light btn-hover-primary mr-1" t:data-tippy-content="Edit"><wicket:svg href="edit" class="icon"></wicket:svg></a>
<a wicket:id="impersonate" t:data-tippy-content="Impersonate" class="btn btn-xs btn-icon btn-light btn-hover-primary mr-1"><wicket:svg href="user-tick" class="icon align-middle"/></a>
</wicket:fragment>
<wicket:fragment wicket:id="emailFrag">
<span wicket:id="emailAddress"></span>

View File

@ -65,6 +65,7 @@ import io.onedev.server.web.component.user.UserAvatar;
import io.onedev.server.web.page.HomePage;
import io.onedev.server.web.page.admin.AdministrationPage;
import io.onedev.server.web.page.user.UserCssResourceReference;
import io.onedev.server.web.page.user.basicsetting.UserBasicSettingPage;
import io.onedev.server.web.page.user.profile.UserProfilePage;
import io.onedev.server.web.util.LoadableDetachableDataProvider;
import io.onedev.server.web.util.WicketUtils;
@ -839,7 +840,7 @@ public class UserListPage extends AdministrationPage {
IModel<User> rowModel) {
User user = rowModel.getObject();
Fragment fragment = new Fragment(componentId, "nameFrag", UserListPage.this);
WebMarkupContainer link = new ActionablePageLink("link", UserProfilePage.class, UserProfilePage.paramsOf(user)) {
var link = new ActionablePageLink("link", UserProfilePage.class, UserProfilePage.paramsOf(user)) {
@Override
protected void doBeforeNav(AjaxRequestTarget target) {
@ -847,7 +848,7 @@ public class UserListPage extends AdministrationPage {
UserListPage.class, getPageParameters()).toString();
WebSession.get().setRedirectUrlAfterDelete(User.class, redirectUrlAfterDelete);
}
};
link.add(new UserAvatar("avatar", user));
link.add(new Label("name", user.getName()));
@ -920,6 +921,17 @@ public class UserListPage extends AdministrationPage {
public void populateItem(Item<ICellPopulator<User>> cellItem, String componentId, IModel<User> rowModel) {
Fragment fragment = new Fragment(componentId, "actionsFrag", UserListPage.this);
fragment.add(new ActionablePageLink("edit", UserBasicSettingPage.class, UserBasicSettingPage.paramsOf(rowModel.getObject())) {
@Override
protected void doBeforeNav(AjaxRequestTarget target) {
String redirectUrlAfterDelete = RequestCycle.get().urlFor(
UserListPage.class, getPageParameters()).toString();
WebSession.get().setRedirectUrlAfterDelete(User.class, redirectUrlAfterDelete);
}
});
fragment.add(new Link<Void>("impersonate") {
@Override

View File

@ -1,8 +1,6 @@
package io.onedev.server.web.page.base;
import static io.onedev.server.web.behavior.ChangeObserver.filterObservables;
import static io.onedev.server.web.page.admin.ssosetting.SsoProcessPage.MOUNT_PATH;
import static io.onedev.server.web.page.admin.ssosetting.SsoProcessPage.STAGE_INITIATE;
import static io.onedev.server.web.translation.Translation._T;
import static org.apache.wicket.ajax.attributes.CallbackParameter.explicit;
@ -47,7 +45,6 @@ import org.apache.wicket.protocol.ws.api.WebSocketRequestHandler;
import org.apache.wicket.protocol.ws.api.message.TextMessage;
import org.apache.wicket.request.IRequestParameters;
import org.apache.wicket.request.cycle.RequestCycle;
import org.apache.wicket.request.flow.RedirectToUrlException;
import org.apache.wicket.request.http.WebRequest;
import org.apache.wicket.request.http.WebResponse;
import org.apache.wicket.request.mapper.parameter.PageParameters;
@ -68,7 +65,6 @@ import io.onedev.commons.loader.AppLoader;
import io.onedev.server.OneDev;
import io.onedev.server.commandhandler.Upgrade;
import io.onedev.server.entitymanager.AuditManager;
import io.onedev.server.entitymanager.SettingManager;
import io.onedev.server.model.User;
import io.onedev.server.security.SecurityUtils;
import io.onedev.server.util.CryptoUtils;
@ -80,11 +76,10 @@ import io.onedev.server.web.behavior.ForceOrdinaryStyleBehavior;
import io.onedev.server.web.behavior.ZoneIdBehavior;
import io.onedev.server.web.component.svg.SpriteImage;
import io.onedev.server.web.editable.BeanEditor;
import io.onedev.server.web.page.admin.ssosetting.SsoProcessPage;
import io.onedev.server.web.page.help.IncompatibilitiesPage;
import io.onedev.server.web.page.security.LoginPage;
import io.onedev.server.web.page.serverinit.ServerInitPage;
import io.onedev.server.web.page.simple.SimplePage;
import io.onedev.server.web.page.simple.security.LoginPage;
import io.onedev.server.web.page.simple.serverinit.ServerInitPage;
import io.onedev.server.web.util.WicketUtils;
import io.onedev.server.web.websocket.WebSocketManager;
import io.onedev.server.web.websocket.WebSocketMessages;
@ -151,17 +146,6 @@ public abstract class BasePage extends WebPage {
protected void onInitialize() {
super.onInitialize();
if (getLoginUser() == null) {
Cookie cookie = SsoProcessPage.getConnectorCookie();
if (cookie != null) {
SsoProcessPage.clearConnectorCookie();
new RestartResponseAtInterceptPageException(getPage().getClass());
String serverUrl = OneDev.getInstance(SettingManager.class).getSystemSetting().getServerUrl();
String redirectUrl = serverUrl + "/" + MOUNT_PATH + "/" + STAGE_INITIATE + "/" + cookie.getValue();
throw new RedirectToUrlException(redirectUrl);
}
}
if (!isPermitted())
unauthorized();
@ -292,7 +276,7 @@ public abstract class BasePage extends WebPage {
};
if (getPage() instanceof SimplePage && getPage().visitChildren(BeanEditor.class, visitor) != null)
builder.append("force-ordinary-style ");
builder.append(" force-ordinary-style ");
return String.format("$('html').addClass('%s');", builder.toString());
}

View File

@ -1,4 +1,4 @@
package io.onedev.server.web.page.simple.error;
package io.onedev.server.web.page.error;
import static io.onedev.server.web.translation.Translation._T;
import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;

View File

@ -1,4 +1,4 @@
package io.onedev.server.web.page.simple.error;
package io.onedev.server.web.page.error;
import static io.onedev.server.web.translation.Translation._T;

View File

@ -1,4 +1,4 @@
package io.onedev.server.web.page.simple.error;
package io.onedev.server.web.page.error;
import static io.onedev.server.web.translation.Translation._T;

View File

@ -103,9 +103,9 @@
<wicket:svg href="shield" class="icon mr-2"></wicket:svg>
<wicket:t>Two-factor Authentication</wicket:t>
</a>
<a wicket:id="myQueryWatches" class="dropdown-item">
<a wicket:id="mySsoAccounts" class="dropdown-item">
<wicket:svg href="bell" class="icon mr-2"></wicket:svg>
<wicket:t>Query Watches</wicket:t>
<wicket:t>SSO Accounts</wicket:t>
</a>
<a wicket:id="signOut" class="dropdown-item">
<wicket:svg href="logout" class="icon mr-2"></wicket:svg>

View File

@ -141,7 +141,7 @@ import io.onedev.server.web.page.admin.serverinformation.ServerInformationPage;
import io.onedev.server.web.page.admin.serverlog.ServerLogPage;
import io.onedev.server.web.page.admin.servicedesk.ServiceDeskSettingPage;
import io.onedev.server.web.page.admin.sshserverkey.SshServerKeyPage;
import io.onedev.server.web.page.admin.ssosetting.SsoConnectorListPage;
import io.onedev.server.web.page.admin.ssosetting.SsoProviderListPage;
import io.onedev.server.web.page.admin.systemsetting.SystemSettingPage;
import io.onedev.server.web.page.admin.usermanagement.InvitationListPage;
import io.onedev.server.web.page.admin.usermanagement.NewInvitationPage;
@ -159,9 +159,10 @@ import io.onedev.server.web.page.my.password.MyPasswordPage;
import io.onedev.server.web.page.my.profile.MyProfilePage;
import io.onedev.server.web.page.my.querywatch.MyQueryWatchesPage;
import io.onedev.server.web.page.my.sshkeys.MySshKeysPage;
import io.onedev.server.web.page.my.ssoaccounts.MySsoAccountsPage;
import io.onedev.server.web.page.my.twofactorauthentication.MyTwoFactorAuthenticationPage;
import io.onedev.server.web.page.simple.security.LoginPage;
import io.onedev.server.web.page.simple.security.LogoutPage;
import io.onedev.server.web.page.security.LoginPage;
import io.onedev.server.web.page.security.LogoutPage;
import io.onedev.server.web.page.user.UserPage;
import io.onedev.server.web.util.WicketUtils;
@ -231,12 +232,12 @@ public abstract class LayoutPage extends BasePage {
new PageParameters(), Lists.newArrayList(NewGroupPage.class, GroupPage.class)));
List<SidebarMenuItem> authenticationMenuItems = new ArrayList<>();
authenticationMenuItems.add(new SidebarMenuItem.Page(null, _T("Authenticator"),
authenticationMenuItems.add(new SidebarMenuItem.Page(null, _T("Password Authenticator"),
AuthenticatorPage.class, new PageParameters()));
authenticationMenuItems.add(new SidebarMenuItem.Page(null, _T("Single Sign On"),
SsoConnectorListPage.class, new PageParameters()));
SsoProviderListPage.class, new PageParameters()));
administrationMenuItems.add(new SidebarMenuItem.SubMenu(null, _T("External Auth Source"), authenticationMenuItems));
administrationMenuItems.add(new SidebarMenuItem.SubMenu(null, _T("External Authentication"), authenticationMenuItems));
var sshPort = OneDev.getInstance(ServerConfig.class).getSshPort();
List<SidebarMenuItem> keyManagementMenuItems = new ArrayList<>();
@ -1142,6 +1143,14 @@ public abstract class LayoutPage extends BasePage {
userInfo.add(new WebMarkupContainer("myTwoFactorAuthentication").setVisible(false));
}
if (loginUser != null && !loginUser.isDisabled() && !loginUser.isServiceAccount()) {
userInfo.add(item = new ViewStateAwarePageLink<Void>("mySsoAccounts", MySsoAccountsPage.class));
if (getPage() instanceof MySsoAccountsPage)
item.add(AttributeAppender.append("class", "active"));
} else {
userInfo.add(new WebMarkupContainer("mySsoAccounts").setVisible(false));
}
if (getLoginUser() != null && !getLoginUser().isServiceAccount() && !getLoginUser().isDisabled()) {
userInfo.add(item = new ViewStateAwarePageLink<Void>("myQueryWatches", MyQueryWatchesPage.class));
if (getPage() instanceof MyQueryWatchesPage)

View File

@ -7,7 +7,7 @@ import io.onedev.server.OneDev;
import io.onedev.server.entitymanager.SettingManager;
import io.onedev.server.model.User;
import io.onedev.server.web.page.layout.LayoutPage;
import io.onedev.server.web.page.simple.security.LoginPage;
import io.onedev.server.web.page.security.LoginPage;
import io.onedev.server.web.util.UserAware;
public abstract class MyPage extends LayoutPage implements UserAware {

View File

@ -0,0 +1,3 @@
<wicket:extend>
<div wicket:id="accountList"></div>
</wicket:extend>

View File

@ -0,0 +1,41 @@
package io.onedev.server.web.page.my.ssoaccounts;
import static io.onedev.server.web.translation.Translation._T;
import org.apache.wicket.Component;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.model.LoadableDetachableModel;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import io.onedev.server.model.User;
import io.onedev.server.web.component.user.ssoaccount.SsoAccountListPanel;
import io.onedev.server.web.page.my.MyPage;
public class MySsoAccountsPage extends MyPage {
public MySsoAccountsPage(PageParameters params) {
super(params);
if (getUser().isDisabled() || getUser().isServiceAccount())
throw new IllegalStateException();
}
@Override
protected void onInitialize() {
super.onInitialize();
add(new SsoAccountListPanel("accountList", new LoadableDetachableModel<User>() {
@Override
protected User load() {
return getLoginUser();
}
}));
}
@Override
protected Component newTopbarTitle(String componentId) {
return new Label(componentId, _T("My SSO Accounts"));
}
}

View File

@ -109,7 +109,7 @@ import io.onedev.server.web.page.project.setting.webhook.WebHooksPage;
import io.onedev.server.web.page.project.stats.code.CodeContribsPage;
import io.onedev.server.web.page.project.stats.code.SourceLinesPage;
import io.onedev.server.web.page.project.tags.ProjectTagsPage;
import io.onedev.server.web.page.simple.security.LoginPage;
import io.onedev.server.web.page.security.LoginPage;
import io.onedev.server.web.util.ProjectAware;
public abstract class ProjectPage extends LayoutPage implements ProjectAware {

View File

@ -36,7 +36,7 @@ import io.onedev.server.web.page.project.ProjectPage;
import io.onedev.server.web.page.project.dashboard.ProjectDashboardPage;
import io.onedev.server.web.page.project.issues.detail.IssueActivitiesPage;
import io.onedev.server.web.page.project.issues.list.ProjectIssueListPage;
import io.onedev.server.web.page.simple.security.LoginPage;
import io.onedev.server.web.page.security.LoginPage;
public class NewIssuePage extends ProjectPage implements InputContext {

View File

@ -113,7 +113,7 @@ import io.onedev.server.web.page.project.dashboard.ProjectDashboardPage;
import io.onedev.server.web.page.project.pullrequests.ProjectPullRequestsPage;
import io.onedev.server.web.page.project.pullrequests.detail.PullRequestDetailPage;
import io.onedev.server.web.page.project.pullrequests.detail.activities.PullRequestActivitiesPage;
import io.onedev.server.web.page.simple.security.LoginPage;
import io.onedev.server.web.page.security.LoginPage;
import io.onedev.server.web.util.TextUtils;
import io.onedev.server.web.util.editbean.LabelsBean;

View File

@ -65,7 +65,7 @@ import io.onedev.server.web.page.project.pullrequests.detail.PullRequestDetailPa
import io.onedev.server.web.page.project.pullrequests.detail.activities.activity.PullRequestChangeActivity;
import io.onedev.server.web.page.project.pullrequests.detail.activities.activity.PullRequestCommentActivity;
import io.onedev.server.web.page.project.pullrequests.detail.activities.activity.PullRequestUpdateActivity;
import io.onedev.server.web.page.simple.security.LoginPage;
import io.onedev.server.web.page.security.LoginPage;
public class PullRequestActivitiesPage extends PullRequestDetailPage {

View File

@ -1,4 +1,4 @@
package io.onedev.server.web.page.simple.security;
package io.onedev.server.web.page.security;
import static io.onedev.server.web.translation.Translation._T;

View File

@ -1,4 +1,4 @@
package io.onedev.server.web.page.simple.security;
package io.onedev.server.web.page.security;
import static io.onedev.server.web.translation.Translation._T;

View File

@ -0,0 +1,54 @@
package io.onedev.server.web.page.security;
import java.io.Serializable;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import javax.validation.constraints.NotEmpty;
import io.onedev.server.OneDev;
import io.onedev.server.annotation.Editable;
import io.onedev.server.annotation.Password;
import io.onedev.server.annotation.UserChoice;
import io.onedev.server.entitymanager.UserManager;
import io.onedev.server.model.User;
@Editable
public class LinkUserBean implements Serializable {
private String userName;
private String password;
@Editable(order=100, name="User", description="Only users able to authenticate via password can be linked")
@UserChoice("getLinkableUsers")
@NotEmpty
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
@Editable(order=200, description="Password of the user")
@Password(autoComplete="current-password")
@NotEmpty
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@SuppressWarnings("unused")
private static List<User> getLinkableUsers() {
return OneDev.getInstance(UserManager.class).query().stream()
.filter(it -> !it.isServiceAccount() && !it.isDisabled())
.sorted(Comparator.comparing(User::getDisplayName))
.collect(Collectors.toList());
}
}

View File

@ -1,7 +1,7 @@
package io.onedev.server.web.page.simple.security;
package io.onedev.server.web.page.security;
import static io.onedev.server.web.page.admin.ssosetting.SsoProcessPage.MOUNT_PATH;
import static io.onedev.server.web.page.admin.ssosetting.SsoProcessPage.STAGE_INITIATE;
import static io.onedev.server.web.page.security.SsoProcessPage.MOUNT_PATH;
import static io.onedev.server.web.page.security.SsoProcessPage.STAGE_INITIATE;
import static io.onedev.server.web.translation.Translation._T;
import java.text.MessageFormat;
@ -33,10 +33,11 @@ import org.apache.wicket.request.mapper.parameter.PageParameters;
import io.onedev.server.OneDev;
import io.onedev.server.entitymanager.SettingManager;
import io.onedev.server.entitymanager.SsoProviderManager;
import io.onedev.server.entitymanager.UserManager;
import io.onedev.server.model.SsoProvider;
import io.onedev.server.model.User;
import io.onedev.server.model.support.administration.BrandingSetting;
import io.onedev.server.model.support.administration.sso.SsoConnector;
import io.onedev.server.security.SecurityUtils;
import io.onedev.server.security.realm.PasswordAuthenticatingRealm;
import io.onedev.server.web.component.link.ViewStateAwarePageLink;
@ -175,15 +176,17 @@ public class LoginPage extends SimplePage {
String serverUrl = settingManager.getSystemSetting().getServerUrl();
var ssoProviderManager = OneDev.getInstance(SsoProviderManager.class);
RepeatingView ssoButtonsView = new RepeatingView("ssoButtons");
for (SsoConnector connector: settingManager.getSsoConnectors()) {
var ssoProviders = ssoProviderManager.query();
for (SsoProvider provider: ssoProviders) {
ExternalLink ssoButton = new ExternalLink(ssoButtonsView.newChildId(),
Model.of(serverUrl + "/" + MOUNT_PATH + "/" + STAGE_INITIATE + "/" + connector.getName()));
ssoButton.add(new ExternalImage("image", connector.getButtonImageUrl()));
ssoButton.add(new Label("label", MessageFormat.format(_T("Login with {0}"), connector.getName())));
Model.of(serverUrl + "/" + MOUNT_PATH + "/" + STAGE_INITIATE + "/" + provider.getName()));
ssoButton.add(new ExternalImage("image", provider.getConnector().getButtonImageUrl()));
ssoButton.add(new Label("label", MessageFormat.format(_T("Login with {0}"), provider.getName())));
ssoButtonsView.add(ssoButton);
}
fragment.add(ssoButtonsView.setVisible(!settingManager.getSsoConnectors().isEmpty()));
fragment.add(ssoButtonsView.setVisible(!ssoProviders.isEmpty()));
add(fragment);

View File

@ -1,4 +1,4 @@
package io.onedev.server.web.page.simple.security;
package io.onedev.server.web.page.security;
import io.onedev.server.web.WebSession;
import io.onedev.server.web.page.base.BasePage;

View File

@ -1,4 +1,4 @@
package io.onedev.server.web.page.simple.security;
package io.onedev.server.web.page.security;
import static io.onedev.server.web.translation.Translation._T;

Some files were not shown because too many files have changed in this diff Show More