Skip to content

Commit

Permalink
Enable sync groups from OAuth server
Browse files Browse the repository at this point in the history
Perform sync group based on group whitelist
Signed-off-by: Bharath Sekar <bsekar@guidewire.com>
Signed-off-by: Ghata Khasakia <gkhasakia@guidewire.com>
Signed-off-by: Mark Huang <mhuang@guidewire.com>
  • Loading branch information
bsekar authored and pwielgolaski committed Jul 11, 2020
1 parent 6e19899 commit 2c8a9cf
Show file tree
Hide file tree
Showing 16 changed files with 429 additions and 52 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,4 @@ data/
servers/
downloads/
auth-config.xml
*.DS_Store
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class AuthenticationSchemeProperties {

Expand Down Expand Up @@ -68,6 +71,25 @@ public List<String> getEmailDomains() {
return StringUtil.split(getProperty(ConfigKey.emailDomain), " ");
}

@Nullable
public Set<String> getWhitelistedGroups() {
String groupsProperty = getProperty(ConfigKey.groups);
Set<String> groups = null;

if (StringUtil.isNotEmpty(groupsProperty)) {
groups = Stream.of(groupsProperty.split(","))
.map(String::trim)
.collect(Collectors.toSet());
}
return groups;
}

public boolean isSyncGroups() {
return Optional.ofNullable(getProperty(ConfigKey.syncGroups))
.map(Boolean::valueOf)
.orElse(true);
}

@Nullable
public String getOrganizations() {
return getProperty(ConfigKey.organizations);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,7 @@ public enum ConfigKey {
hideLoginForm,
allowInsecureHttps,
emailDomain,
organizations
organizations,
groups,
syncGroups
}
50 changes: 39 additions & 11 deletions src/main/java/jetbrains/buildServer/auth/oauth/OAuthUser.java
Original file line number Diff line number Diff line change
@@ -1,40 +1,43 @@
package jetbrains.buildServer.auth.oauth;

import com.intellij.openapi.util.text.StringUtil;
import org.json.simple.JSONArray;

import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.*;

public class OAuthUser {
private static final String[] IDS_LIST = new String[]{"login", "username", "id", "preferred_username"};
private static final String[] NAMES_LIST = new String[]{"name", "display_name", "displayName"};
private static final String[] EMAIL_LIST = new String[]{"email", "mail"};
private final String GROUPS_KEY = "groups";

private final String id;
private final String name;
private final String email;
private final List<String> groups;

public OAuthUser(String id) {
this(id, null, null);
this(id, null, null, null);
}

public OAuthUser(String id, String name, String email) {
public OAuthUser(String id, String name, String email, List<String> groups) {
this.id = id;
this.name = name;
this.email = email;
this.groups = groups;
}

public OAuthUser(Map userData) {
this.id = getValueByKeys(userData, IDS_LIST);
this.name = getValueByKeys(userData, NAMES_LIST);
this.email = getValueByKeys(userData, EMAIL_LIST);
this.groups = getGroups(userData, GROUPS_KEY) == null ? new ArrayList<>() : Arrays.asList(getGroups(userData,
GROUPS_KEY));
}

private String getValueByKeys(Map userData, String[] keys) {
if (userData == null)
if (userData == null) {
return null;
}
String value = null;
for (String key : keys) {
value = (String) userData.get(key);
Expand All @@ -45,6 +48,23 @@ private String getValueByKeys(Map userData, String[] keys) {
return value;
}

private String[] getGroups(Map userData, String key) {
if (userData == null) {
return null;
}
Object groupsObject = userData.get(key);
if (groupsObject == null) {
return null;
}
JSONArray groupsJsonArray = (JSONArray) groupsObject;
int groupSize = groupsJsonArray.size();
String[] groupsArray = new String[groupSize];
for (int i = 0; i < groupSize; i++) {
groupsArray[i] = (String) groupsJsonArray.get(i);
}
return groupsArray;
}

public String getId() {
return Optional.ofNullable(id).orElse(email);
}
Expand All @@ -63,7 +83,7 @@ public void validate(AuthenticationSchemeProperties properties) throws Exception
}
List<String> emailDomains = properties.getEmailDomains();

if(emailDomains != null && !emailDomains.isEmpty()) {
if (emailDomains != null && !emailDomains.isEmpty()) {
boolean isValid = emailDomains.stream().anyMatch(emailDomain -> {
if (!emailDomain.startsWith("@")) {
emailDomain = "@" + emailDomain;
Expand All @@ -88,8 +108,12 @@ public String toString() {

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
OAuthUser oAuthUser = (OAuthUser) o;
return Objects.equals(id, oAuthUser.id) &&
Objects.equals(name, oAuthUser.name) &&
Expand All @@ -100,4 +124,8 @@ public boolean equals(Object o) {
public int hashCode() {
return Objects.hash(id, name, email);
}

public List<String> getGroups() {
return groups;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package jetbrains.buildServer.auth.oauth;

import jetbrains.buildServer.controllers.AuthorizationInterceptor;
import jetbrains.buildServer.groups.UserGroupManager;
import jetbrains.buildServer.serverSide.SBuildServer;
import jetbrains.buildServer.serverSide.auth.LoginConfiguration;
import jetbrains.buildServer.users.UserModel;
Expand All @@ -24,8 +25,8 @@ public OAuthClient oAuthClient(AuthenticationSchemeProperties properties) {
}

@Bean
public ServerPrincipalFactory serverPrincipalFactory(UserModel userModel) {
return new ServerPrincipalFactory(userModel);
public ServerPrincipalFactory serverPrincipalFactory(UserModel userModel, UserGroupManager userGroupManager, AuthenticationSchemeProperties properties) {
return new ServerPrincipalFactory(userModel, userGroupManager, properties);
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
package jetbrains.buildServer.auth.oauth;

import jetbrains.buildServer.groups.SUserGroup;
import jetbrains.buildServer.groups.UserGroupManager;
import jetbrains.buildServer.serverSide.auth.ServerPrincipal;
import jetbrains.buildServer.users.InvalidUsernameException;
import jetbrains.buildServer.users.SUser;
import jetbrains.buildServer.users.UserModel;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.Optional;

import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Collectors;

public class ServerPrincipalFactory {

Expand All @@ -18,36 +20,108 @@ public class ServerPrincipalFactory {
@NotNull
private final UserModel userModel;

public ServerPrincipalFactory(@NotNull UserModel userModel) {
@NotNull
private final UserGroupManager userGroupManager;

@NotNull
private final AuthenticationSchemeProperties properties;

public ServerPrincipalFactory(@NotNull UserModel userModel, @NotNull UserGroupManager userGroupManager,
@NotNull AuthenticationSchemeProperties properties) {
this.userModel = userModel;
this.userGroupManager = userGroupManager;
this.properties = properties;
}

@NotNull
public Optional<ServerPrincipal> getServerPrincipal(@NotNull final OAuthUser user, boolean allowCreatingNewUsersByLogin) {
Optional<ServerPrincipal> existingPrincipal = findExistingPrincipal(user.getId());
if (existingPrincipal.isPresent()) {
public Optional<ServerPrincipal> getServerPrincipal(@NotNull final OAuthUser user,
boolean allowCreatingNewUsersByLogin) {
Optional<SUser> existingUserOptional = findExistingUser(user.getId());
boolean syncGroups = properties.isSyncGroups();
List<String> groupsInToken = user.getGroups();
if (existingUserOptional.isPresent()) {
LOG.info("Use existing user: " + user.getId());
return existingPrincipal;
SUser existingUser = existingUserOptional.get();
if (syncGroups) {
//user group or all user groups?
List<String> existingUserGroups =
existingUser.getUserGroups().stream().map(userGroup -> userGroup.getName())
.collect(Collectors.toList());
List<String> additionalGroupsInToken = obtainGroupsDelta(groupsInToken, existingUserGroups);
addUserToGroups(existingUser, applyGroupWhitelist(additionalGroupsInToken));

List<String> groupsNotPresentInToken = obtainGroupsDelta(existingUserGroups, groupsInToken);
removeUserFromGroups(existingUser, applyGroupWhitelist(groupsNotPresentInToken));
}
return Optional.of(new ServerPrincipal(PluginConstants.OAUTH_AUTH_SCHEME_NAME, existingUser.getUsername()));
} else if (allowCreatingNewUsersByLogin) {
LOG.info("Creating user: " + user);
SUser created = userModel.createUserAccount(PluginConstants.OAUTH_AUTH_SCHEME_NAME, user.getId());
if (syncGroups) {
addUserToGroups(created, applyGroupWhitelist(groupsInToken));
}
created.setUserProperty(PluginConstants.ID_USER_PROPERTY_KEY, user.getId());
created.updateUserAccount(user.getId(), user.getName(), user.getEmail());
return Optional.of(new ServerPrincipal(PluginConstants.OAUTH_AUTH_SCHEME_NAME, user.getId()));
} else {
LOG.info("User: " + user + " could not be found and allowCreatingNewUsersByLogin is disabled");
return existingPrincipal;
return Optional.empty();
}
}

private void removeUserFromGroups(SUser existingUser, List<String> groupsToDelete) {
for (String group : groupsToDelete) {
SUserGroup userGroup = userGroupManager.findUserGroupByName(group);
if (userGroup != null) {
userGroup.removeUser(existingUser);
}
}
}

private List<String> applyGroupWhitelist(List<String> groups) {
List<String> finalGroupList = new ArrayList<>();
Set<String> whitelistedGroups = properties.getWhitelistedGroups();
if (whitelistedGroups != null) {
for (String group : groups) {
for (String whitelistedGroup : whitelistedGroups) {
if (group.startsWith(whitelistedGroup)) {
finalGroupList.add(group);
break;
}
}
}
}
return finalGroupList;
}

private void addUserToGroups(SUser created, List<String> finalGroupList) {
for (String group : finalGroupList) {
SUserGroup userGroup = userGroupManager.findUserGroupByName(group);
if (userGroup != null) {
userGroup.addUser(created);
}
}
}

@NotNull
private Optional<ServerPrincipal> findExistingPrincipal(@NotNull final String userName) {
private Optional<SUser> findExistingUser(@NotNull final String userName) {
try {
final SUser user = userModel.findUserByUsername(userName, PluginConstants.ID_USER_PROPERTY_KEY);
return Optional.ofNullable(user).map(u -> new ServerPrincipal(PluginConstants.OAUTH_AUTH_SCHEME_NAME, user.getUsername()));
return Optional.ofNullable(user);
} catch (InvalidUsernameException e) {
// ignore it
return Optional.empty();
}
}

private List<String> obtainGroupsDelta(List<String> minuend, List<String> subtrahend) {
return minuend.stream()
.filter(filterOutEqualValuesCaseInsensitive(subtrahend))
.collect(Collectors.toList());
}

private Predicate<String> filterOutEqualValuesCaseInsensitive(Collection<String> valuesToExclude) {
return valueUnderTest -> valuesToExclude.stream().map(String::toLowerCase)
.noneMatch(s -> s.equals(valueUnderTest.toLowerCase()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
<jsp:include page="/admin/allowCreatingNewUsersByLogin.jsp"/>
</div>
<br/>
<style type="text/css">
div.posRel {
width: 95%;
}
</style>
<script type="text/javascript">
(function () {
BS.TeamCityOAuth = {
Expand All @@ -20,13 +25,25 @@
},
onTypeChanged: function () {
var isCustom = (this.selectedPreset === 'custom'),
settings = $j('#oauth_server_settings');
settings = $j('#oauth_server_settings'),
groupSettings = $j('#oauth_server_group_settings');
if (isCustom) {
settings.show();
groupSettings.show();
} else {
settings.hide();
groupSettings.hide();
}
},
showOrHideGroups: function () {
var groupsListDiv = $j('#groups_list');
var syncGroups = $j('#<%=ConfigKey.syncGroups.toString()%>');
if (syncGroups[0].checked) {
groupsListDiv.show();
} else {
groupsListDiv.hide();
}
},
displayOrganizations: function() {
var isGithub = (this.selectedPreset === 'github'),
orgs = $j('#github_organizations');
Expand All @@ -37,6 +54,10 @@
}
}
};
$j(document).ready(function() {
BS.TeamCityOAuth.init('#<%=ConfigKey.preset.toString()%>');
BS.TeamCityOAuth.showOrHideGroups();
});
})();
</script>
<div>
Expand Down Expand Up @@ -98,13 +119,23 @@
<div>
<prop:checkboxProperty uncheckedValue="false" name="<%=ConfigKey.hideLoginForm.toString()%>"/>
<label for="<%=ConfigKey.hideLoginForm%>">Hide login form</label><br/>
<span class="grayNote">Hide user/password login form on Teamcity login page.</span>
<span class="grayNote">Hide user/password login form on TeamCity login page.</span>
</div>
<div>
<prop:checkboxProperty uncheckedValue="false" name="<%=ConfigKey.allowInsecureHttps.toString()%>"/>
<label for="<%=ConfigKey.allowInsecureHttps%>">Insecure https</label><br/>
<span class="grayNote">Allow insecure https access like invalid certificate</span>
</div>
<script type="text/javascript">
BS.TeamCityOAuth.init('#<%=ConfigKey.preset.toString()%>');
</script>
<div id="oauth_server_group_settings">
<div>
<prop:checkboxProperty name="<%=ConfigKey.syncGroups.toString()%>" checked="false" uncheckedValue="false"
onclick="BS.TeamCityOAuth.showOrHideGroups()"/>
<label for="<%=ConfigKey.syncGroups%>">Sync groups</label><br/>
<span class="grayNote">Allow synchronization of groups based on the <i>groups</i> claim in the access token.</span>
</div>
<div id="groups_list" style="display: none;">
<label for="<%=ConfigKey.groups%>">Groups (starts with):</label><br/>
<prop:textProperty style="width: 100%;" name="<%=ConfigKey.groups.toString()%>"/><br/>
<span class="grayNote">The groups that are allowed to be managed by this OAuth server. Please separate the groups or group prefixes by commas, e.g., "dev-team1, dev-team2" or "dev-".</span>
</div>
</div>
Loading

0 comments on commit 2c8a9cf

Please sign in to comment.