diff --git a/ad-connector/pom.xml b/ad-connector/pom.xml index 36286b000bc6dccd262b3c7affcc96ce6ae6743e..7c551eb83c66ed718d6c02af4df0a5c6aeff74ce 100644 --- a/ad-connector/pom.xml +++ b/ad-connector/pom.xml @@ -27,6 +27,12 @@ <groupId>com.imperva.ddc</groupId> <version>${imperva.ddc.version}</version> </dependency> + <dependency> + <groupId>net.jami</groupId> + <artifactId>jams-server</artifactId> + <version>2.0</version> + <scope>compile</scope> + </dependency> </dependencies> <build> diff --git a/ad-connector/src/main/java/net/jami/jams/ad/connector/ADConnector.java b/ad-connector/src/main/java/net/jami/jams/ad/connector/ADConnector.java index 80d93744d8e335c7e6d8e072ef4d6937dc03e98b..7511476c4a0508149df3cc3c973ed1850bc33aab 100644 --- a/ad-connector/src/main/java/net/jami/jams/ad/connector/ADConnector.java +++ b/ad-connector/src/main/java/net/jami/jams/ad/connector/ADConnector.java @@ -37,6 +37,7 @@ import net.jami.jams.common.objects.user.UserProfile; import net.jami.jams.common.serialization.JsoniterRegistry; import java.util.List; +import java.util.Optional; import java.util.concurrent.ConcurrentLinkedQueue; @Slf4j @@ -93,13 +94,13 @@ public class ADConnector implements AuthenticationSource { } @Override - public List<UserProfile> searchUserProfiles(String queryString, String field) { - return userProfileService.getUserProfile(queryString, field,false); + public List<UserProfile> searchUserProfiles(String queryString, String field, Optional<Integer> page) { + return userProfileService.getUserProfile(queryString, field, false, page); } @Override public UserProfile getUserProfile(String username) { - List<UserProfile> result = userProfileService.getUserProfile(username, "LOGON_NAME",true); + List<UserProfile> result = userProfileService.getUserProfile(username, "LOGON_NAME",true, Optional.empty()); if(result == null || result.size() != 1) return null; return result.get(0); } diff --git a/ad-connector/src/main/java/net/jami/jams/ad/connector/service/UserProfileService.java b/ad-connector/src/main/java/net/jami/jams/ad/connector/service/UserProfileService.java index f2080882668fe0b60014fe1eef6e5cd3d72f3cdb..9a99d5aa9ee2a6b595a51384ed93b97dee250df0 100644 --- a/ad-connector/src/main/java/net/jami/jams/ad/connector/service/UserProfileService.java +++ b/ad-connector/src/main/java/net/jami/jams/ad/connector/service/UserProfileService.java @@ -38,18 +38,22 @@ import com.imperva.ddc.core.query.QueryRequest; import com.imperva.ddc.core.query.QueryResponse; import lombok.extern.slf4j.Slf4j; import net.jami.jams.ad.connector.ADConnector; +import net.jami.jams.common.dao.StatementElement; +import net.jami.jams.common.dao.StatementList; import net.jami.jams.common.objects.user.UserProfile; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Objects; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; +import static net.jami.jams.server.Server.dataStore; + @Slf4j public class UserProfileService { - public List<UserProfile> getUserProfile(String queryString, String field,boolean exactMatch) { + private static final ConcurrentHashMap<String, String> fieldMap = ADConnector.settings.getFieldMappings(); + + public List<UserProfile> getUserProfile(String queryString, String field, boolean exactMatch, Optional<Integer> page) { Endpoint endpoint = ADConnector.getConnection(); try { QueryRequest queryRequest = buildRequest(endpoint); @@ -79,8 +83,32 @@ public class UserProfileService { queryResponse = connector.execute(); } List<List<Field>> results = queryResponse.getAll().stream().map(EntityResponse::getValue).collect(Collectors.toList()); - return results.stream().map(UserProfileService::profileFromResponse).collect(Collectors.toList()) - .stream().filter(Objects::nonNull).collect(Collectors.toList()); + + dataStore.NUM_PAGES = (Integer) results.size() / dataStore.RESULTS_PER_PAGE; + if (results.size() % dataStore.RESULTS_PER_PAGE != 0) + dataStore.NUM_PAGES++; + + if (page.isPresent() && !results.isEmpty()) { + if (results.size() < dataStore.RESULTS_PER_PAGE) + results = results.subList(0, results.size()); + else if (page.get() * dataStore.RESULTS_PER_PAGE > results.size()) + results = results.subList((page.get()-1) * dataStore.RESULTS_PER_PAGE, results.size()); + else + results = results.subList((page.get()-1) * dataStore.RESULTS_PER_PAGE, (page.get() * dataStore.RESULTS_PER_PAGE)); + } + + if (results.size() == 0) return new ArrayList<>(); + List<UserProfile> profilesFromResponse = results.stream().map(UserProfileService::profileFromResponse).collect(Collectors.toList()); + for (UserProfile p: profilesFromResponse) { + StatementList statementList = new StatementList(); + StatementElement st = new StatementElement("username", "=", p.getUsername(), ""); + statementList.addStatement(st); + + if (dataStore.getUserProfileDao().getObjects(statementList).isEmpty()) + dataStore.getUserProfileDao().storeObject(p); + } + + return profilesFromResponse; } catch (Exception e) { log.error("Could not find entity with specified parameters."); return null; @@ -90,7 +118,6 @@ public class UserProfileService { } public static QueryRequest buildRequest(Endpoint endpoint) { - HashMap<String, String> fieldMap = ADConnector.settings.getFieldMappings(); QueryRequest queryRequest = new QueryRequest(); queryRequest.setDirectoryType(DirectoryType.MS_ACTIVE_DIRECTORY); queryRequest.setEndpoints(new ArrayList<>() {{ @@ -106,8 +133,19 @@ public class UserProfileService { } public static UserProfile profileFromResponse(List<Field> fields) { - //Use reflection to remap. - HashMap<String, String> fieldMap = ADConnector.settings.getFieldMappings(); + fieldMap.forEach((k, v) -> { + String temp = v; + fieldMap.put(k.toLowerCase(), temp); + }); + fieldMap.forEach((k, v) -> { + char[] charArray = k.toCharArray(); + for(int i=0; i < charArray.length; i++) { + if(Character.isUpperCase( charArray[i] )) { + fieldMap.remove(k); + break; + } + } + }); try { UserProfile userProfile = new UserProfile(); diff --git a/datastore/src/main/java/net/jami/datastore/main/DataStore.java b/datastore/src/main/java/net/jami/datastore/main/DataStore.java index 03cc491dfcb1a77ece20f04709a849235173e2ca..b1f088f3bb57f751e18594642120bc21888481dd 100644 --- a/datastore/src/main/java/net/jami/datastore/main/DataStore.java +++ b/datastore/src/main/java/net/jami/datastore/main/DataStore.java @@ -45,6 +45,7 @@ import org.flywaydb.core.Flyway; import java.util.ArrayList; import java.util.List; +import java.util.Optional; @Getter @Setter @@ -60,6 +61,8 @@ public class DataStore implements AuthenticationSource { private JwtDao jwtDao; private UserProfileDao userProfileDao; private UserGroupMappingsDao userGroupMappingsDao; + public static final Integer RESULTS_PER_PAGE = 24; + public static Integer NUM_PAGES; //Implicitly connect to derby. public DataStore(String connectionString) { @@ -90,7 +93,7 @@ public class DataStore implements AuthenticationSource { return userDao.storeObject(user); } @Override - public List<UserProfile> searchUserProfiles(String queryString, String field) { + public List<UserProfile> searchUserProfiles(String queryString, String field, Optional<Integer> page) { List<UserProfile> userList; if (!queryString.equals("*")) { @@ -110,6 +113,19 @@ public class DataStore implements AuthenticationSource { if (userList == null) userList = new ArrayList<>(); + NUM_PAGES = (Integer) userList.size() / RESULTS_PER_PAGE; + if (userList.size() % RESULTS_PER_PAGE != 0) + NUM_PAGES++; + + if (page.isPresent() && !userList.isEmpty()) { + if (userList.size() < RESULTS_PER_PAGE) + userList = userList.subList(0, userList.size()); + else if (page.get() * RESULTS_PER_PAGE > userList.size()) + userList = userList.subList((page.get()-1) * RESULTS_PER_PAGE, userList.size()); + else + userList = userList.subList((page.get()-1) * RESULTS_PER_PAGE, (page.get() * RESULTS_PER_PAGE)); + } + return userList; } diff --git a/jams-common/src/main/java/net/jami/jams/common/authentication/AuthenticationSource.java b/jams-common/src/main/java/net/jami/jams/common/authentication/AuthenticationSource.java index b65fb69544a79304b952ecd81c1bcc88968388d3..bf01d7edd58b8e7759dfc4768589e9b8a0d79e16 100644 --- a/jams-common/src/main/java/net/jami/jams/common/authentication/AuthenticationSource.java +++ b/jams-common/src/main/java/net/jami/jams/common/authentication/AuthenticationSource.java @@ -26,11 +26,12 @@ import net.jami.jams.common.objects.user.User; import net.jami.jams.common.objects.user.UserProfile; import java.util.List; +import java.util.Optional; public interface AuthenticationSource { boolean createUser(User user); - List<UserProfile> searchUserProfiles(String queryString, String field); + List<UserProfile> searchUserProfiles(String queryString, String field, Optional<Integer> page); UserProfile getUserProfile(String username); boolean setUserProfile(UserProfile userProfile); boolean authenticate(String username, String password); diff --git a/jams-common/src/main/java/net/jami/jams/common/authentication/activedirectory/ActiveDirectorySettings.java b/jams-common/src/main/java/net/jami/jams/common/authentication/activedirectory/ActiveDirectorySettings.java index 7612bee7b1263b19eca032594b30e4d938c5af7c..6eb375211d0e3cf4d6853bed1c19d0b19c05760d 100644 --- a/jams-common/src/main/java/net/jami/jams/common/authentication/activedirectory/ActiveDirectorySettings.java +++ b/jams-common/src/main/java/net/jami/jams/common/authentication/activedirectory/ActiveDirectorySettings.java @@ -26,6 +26,7 @@ import lombok.Getter; import lombok.Setter; import java.util.HashMap; +import java.util.concurrent.ConcurrentHashMap; @Getter @Setter @@ -37,6 +38,6 @@ public class ActiveDirectorySettings { private String host; private String username; private String password; - private HashMap<String,String> fieldMappings; + private ConcurrentHashMap<String,String> fieldMappings; } diff --git a/jams-react-client/src/assets/jss/material-dashboard-react/components/customInputStyle.js b/jams-react-client/src/assets/jss/material-dashboard-react/components/customInputStyle.js index ea3165df7772095a66fa381eb97f799c2c244f2e..d593f7afbf8a0bfe0e2d15cc418c86106f82daa6 100644 --- a/jams-react-client/src/assets/jss/material-dashboard-react/components/customInputStyle.js +++ b/jams-react-client/src/assets/jss/material-dashboard-react/components/customInputStyle.js @@ -3,33 +3,32 @@ import { dangerColor, successColor, grayColor, - defaultFont + defaultFont, } from "assets/jss/material-dashboard-react.js"; - const customInputStyle = { disabled: { "&:before": { - backgroundColor: "transparent !important" - } + backgroundColor: "transparent !important", + }, }, underline: { "&:hover:not($disabled):before,&:before": { borderColor: grayColor[4] + " !important", - borderWidth: "1px !important" + borderWidth: "1px !important", }, "&:after": { - borderColor: primaryColor[0] - } + borderColor: primaryColor[0], + }, }, underlineError: { "&:after": { - borderColor: dangerColor[0] - } + borderColor: dangerColor[0], + }, }, underlineSuccess: { "&:after": { - borderColor: successColor[0] - } + borderColor: successColor[0], + }, }, labelRoot: { ...defaultFont, @@ -37,13 +36,13 @@ const customInputStyle = { fontWeight: "400", fontSize: "14px", lineHeight: "1.42857", - letterSpacing: "unset" + letterSpacing: "unset", }, labelRootError: { - color: dangerColor[0] + color: dangerColor[0], }, labelRootSuccess: { - color: successColor[0] + color: successColor[0], }, feedback: { position: "absolute", @@ -54,17 +53,13 @@ const customInputStyle = { width: "24px", height: "24px", textAlign: "center", - pointerEvents: "none" - }, - marginTop: { - marginTop: "16px" + pointerEvents: "none", }, formControl: { paddingBottom: "10px", - margin: "27px 0 0 0", + // margin: "27px 0 0 0", position: "relative", - verticalAlign: "unset" - } + verticalAlign: "unset", + }, }; - export default customInputStyle; diff --git a/jams-react-client/src/assets/jss/material-dashboard-react/components/headerStyle.js b/jams-react-client/src/assets/jss/material-dashboard-react/components/headerStyle.js index 800d6961e5e4d2f18afa2a2564b2880c16e43c6c..9c6e7d3a08fb5295273b08d67746816c2585c8a4 100644 --- a/jams-react-client/src/assets/jss/material-dashboard-react/components/headerStyle.js +++ b/jams-react-client/src/assets/jss/material-dashboard-react/components/headerStyle.js @@ -22,6 +22,9 @@ const headerStyle = (theme) => ({ [theme.breakpoints.down("md")]: { width: "130px", }, + [theme.breakpoints.up("md")]: { + display: "none", + }, paddingTop: "10px", zIndex: "1029", color: grayColor[7], diff --git a/jams-react-client/src/components/Drawer/Drawer.js b/jams-react-client/src/components/Drawer/Drawer.js index c5fff76e72beed9da02201cf4b7df75d84980a50..342d1d71a36808055bf168ccc4ae4d0b42a503be 100644 --- a/jams-react-client/src/components/Drawer/Drawer.js +++ b/jams-react-client/src/components/Drawer/Drawer.js @@ -86,12 +86,12 @@ export default function TemporaryDrawer(props) { configApiCall( api_path_get_user_directory_search, "GET", - { queryString: "*" }, + { queryString: "*", page: "1" }, null ) ) .then((response) => { - let resp = response.data; + let resp = response.data.profiles; if (!addingToGroup) { let contacts = resp.filter((data) => { if (data.username !== props.username) return data; @@ -193,12 +193,18 @@ export default function TemporaryDrawer(props) { configApiCall( api_path_get_user_directory_search, "GET", - { queryString: value ? value : "*" }, + { queryString: value ? value : "*", page: "1" }, null ) ) .then((response) => { - setUsers(response.data); + let resp = response.data.profiles; + if (!addingToGroup) { + let contacts = resp.filter((data) => { + if (data.username !== props.username) return data; + }); + setUsers(contacts); + } else setUsers(resp); }) .catch((error) => { console.log(error); diff --git a/jams-react-client/src/components/Grid/GridContainer.js b/jams-react-client/src/components/Grid/GridContainer.js index 4c815694f6e2aedf356c93842ea11456a9d71f25..635a2a95de738eea239adc6314485a0db78d3a0a 100644 --- a/jams-react-client/src/components/Grid/GridContainer.js +++ b/jams-react-client/src/components/Grid/GridContainer.js @@ -7,9 +7,9 @@ import Grid from "@material-ui/core/Grid"; const styles = { grid: { - margin: "0 -15px !important", - width: "unset" - } + // margin: "0 -15px !important", + width: "unset", + }, }; const useStyles = makeStyles(styles); @@ -25,5 +25,5 @@ export default function GridContainer(props) { } GridContainer.propTypes = { - children: PropTypes.node + children: PropTypes.node, }; diff --git a/jams-react-client/src/views/Users/Users.js b/jams-react-client/src/views/Users/Users.js index 42f567165fdeacab3dab0033ea5b031bea0367af..c85c8f43db3577eea8cfa7c6b7a21ebea8ae8099 100644 --- a/jams-react-client/src/views/Users/Users.js +++ b/jams-react-client/src/views/Users/Users.js @@ -4,6 +4,7 @@ import { Switch, Route, Redirect } from "react-router-dom"; // @material-ui/core components import { makeStyles } from "@material-ui/core/styles"; import InputLabel from "@material-ui/core/InputLabel"; +import Pagination from "@material-ui/lab/Pagination"; // core components import GridItem from "components/Grid/GridItem.js"; import GridContainer from "components/Grid/GridContainer.js"; @@ -16,33 +17,25 @@ import CardBody from "components/Card/CardBody.js"; import CardFooter from "components/Card/CardFooter.js"; import UserProfile from "views/UserProfile/UserProfile.js"; import Divider from "@material-ui/core/Divider"; - import PersonIcon from "@material-ui/icons/Person"; import PermIdentityIcon from "@material-ui/icons/PermIdentity"; import PhoneOutlinedIcon from "@material-ui/icons/PhoneOutlined"; import InfoIcon from "@material-ui/icons/Info"; import BusinessOutlinedIcon from "@material-ui/icons/BusinessOutlined"; import Search from "@material-ui/icons/Search"; - import MailOutlineIcon from "@material-ui/icons/MailOutline"; import axios from "axios"; import configApiCall from "api.js"; import auth from "auth.js"; import { api_path_get_user_directory_search } from "globalUrls"; - import AddCircleOutlineIcon from "@material-ui/icons/AddCircleOutline"; import KeyboardReturnIcon from "@material-ui/icons/KeyboardReturn"; - import jami from "assets/img/faces/jami.png"; import noProfilePicture from "assets/img/faces/no-profile-picture.png"; import EditCreateUserProfile from "views/UserProfile/EditCreateUserProfile"; - import LinearProgress from "@material-ui/core/LinearProgress"; - import headerLinksStyle from "assets/jss/material-dashboard-react/components/headerLinksStyle.js"; - import { debounce } from "lodash"; - const styles = { ...headerLinksStyle, cardCategoryWhite: { @@ -76,19 +69,23 @@ const styles = { alignItems: "center", }, }; - const useStyles = makeStyles(styles); - export default function Users(props) { const classes = useStyles(); const history = useHistory(); const [users, setUsers] = React.useState([]); const [noUsersFound, setNoUsersFound] = React.useState(false); + const [noMatchFound, setNoMatchFound] = React.useState(false); const [selectedUsername, setSelectedUsername] = React.useState(""); const [createUser, setCreateUser] = React.useState(false); const [loading, setLoading] = React.useState(false); const [progress, setProgress] = React.useState(0); + const [searchValue, setSearchValue] = React.useState(""); + + const [selectedPage, setSelectedPage] = React.useState(1); + + const [numberPages, setNumberPages] = React.useState(1); useEffect(() => { setLoading(true); const timer = setInterval(() => { @@ -104,13 +101,15 @@ export default function Users(props) { configApiCall( api_path_get_user_directory_search, "GET", - { queryString: "*" }, + { queryString: "*", page: "1" }, null ) ) .then((response) => { if (response.status != 204) { - setUsers(response.data); + setUsers(response.data.profiles); + + setNumberPages(response.data.numPages); } else { setNoUsersFound(true); } @@ -127,47 +126,66 @@ export default function Users(props) { clearInterval(timer); }; }, []); - const [selectedProfile, setSelectedProfile] = useState(false); - const redirectToUserProfile = (e, username) => { e.preventDefault(); setSelectedProfile(true); setSelectedUsername(username); }; - - const searchUsers = (value) => { + const searchUsers = (value, page = "1") => { + setSelectedPage(page); + setLoading(true); + setNoMatchFound(false); + setUsers([]); + const timer = setInterval(() => { + setProgress((oldProgress) => { + if (oldProgress === 100) { + return 0; + } + const diff = Math.random() * 10; + return Math.min(oldProgress + diff, 100); + }); + }, 500); axios( configApiCall( api_path_get_user_directory_search, "GET", - { queryString: value ? value : "*" }, + { queryString: value ? value : "*", page: page }, null ) ) .then((response) => { - setUsers(response.data); + setLoading(false); + if (response.status != 204) { + setUsers(response.data.profiles); + setNumberPages(response.data.numPages); + } else { + setNoMatchFound(true); + } }) .catch((error) => { console.log(error); setUsers([]); + + setNoMatchFound(true); if (error.response.status == 401) { auth.authenticated = false; history.push("/"); } }); }; - const initSearchUsers = useCallback( - debounce((searchValue) => searchUsers(searchValue), 500), + debounce((value) => searchUsers(value), 500), [] ); - const handleSearchUsers = (e) => { - const searchValue = e.target.value; - initSearchUsers(searchValue); + setSearchValue(e.target.value); + initSearchUsers(e.target.value); }; + const handleChangePage = (e, page) => { + searchUsers(searchValue, page); + }; if (!auth.hasAdminScope()) { return ( <div> @@ -205,37 +223,51 @@ export default function Users(props) { <AddCircleOutlineIcon /> Create user </Button> )} - <div className={classes.searchWrapper}> - {!noUsersFound && ( - <CustomInput - formControlProps={{ - className: classes.margin + " " + classes.search, - }} - inputProps={{ - placeholder: - "Search users using (username, name, phone, email, ...)", - inputProps: { - "aria-label": "Search users", - }, - onKeyUp: handleSearchUsers, - }} - /> - )} - {!noUsersFound && <Search />} - <div className={classes.loading}> - {loading && ( - <LinearProgress variant="determinate" value={progress} /> + <GridContainer> + <GridItem xs={12} sm={12} md={6}> + {!noUsersFound && ( + <CustomInput + formControlProps={{ + className: classes.margin + " " + classes.search, + }} + inputProps={{ + placeholder: + "Search users using (username, name, phone, email, ...)", + inputProps: { + "aria-label": "Search users", + }, + onKeyUp: handleSearchUsers, + }} + /> )} - </div> - {noUsersFound && ( - <div className={classes.usersNotFound}> - <InfoIcon /> - <p style={{ marginLeft: "10px" }}>No users found</p> - </div> + {!noUsersFound && <Search />} + </GridItem> + <GridItem xs={12} sm={12} md={6}> + {!noUsersFound && ( + <Pagination + count={numberPages} + page={selectedPage} + onChange={handleChangePage} + /> + )} + </GridItem> + </GridContainer> + + <div className={classes.loading}> + {loading && ( + <LinearProgress variant="determinate" value={progress} /> )} </div> + + {noUsersFound && ( + <div className={classes.usersNotFound}> + <InfoIcon /> + + <p style={{ marginLeft: "10px" }}>No users found</p> + </div> + )} </GridItem> - {!noUsersFound && + {(!noUsersFound || !noMatchFound) && users .sort(function (a, b) { if (a.username < b.username) { @@ -271,7 +303,6 @@ export default function Users(props) { alt="..." /> </CardAvatar> - <h4 className={classes.cardTitle}> {user.firstName != "" ? user.firstName @@ -303,6 +334,16 @@ export default function Users(props) { </Card> </GridItem> ))} + {noMatchFound && ( + <div className={classes.usersNotFound}> + <InfoIcon /> + + <p style={{ marginLeft: "10px" }}> + No users found mathcing <strong>{searchValue}</strong> search + value! + </p> + </div> + )} </GridContainer> </div> ); diff --git a/jams-server/src/main/java/net/jami/jams/server/core/workflows/AddUserToGroupFlow.java b/jams-server/src/main/java/net/jami/jams/server/core/workflows/AddUserToGroupFlow.java index ccdf15d46fb57f3ffb9b2c3a5494615ec9b632de..00f57cbf0f973b4aac83eee9bcdf0ead2d862ab7 100644 --- a/jams-server/src/main/java/net/jami/jams/server/core/workflows/AddUserToGroupFlow.java +++ b/jams-server/src/main/java/net/jami/jams/server/core/workflows/AddUserToGroupFlow.java @@ -8,6 +8,7 @@ import net.jami.jams.common.objects.user.UserProfile; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import static net.jami.jams.server.Server.dataStore; import static net.jami.jams.server.Server.userAuthenticationModule; @@ -18,7 +19,7 @@ public class AddUserToGroupFlow { public static void addUserToGroup(String groupName, String username) { userAuthenticationModule.getAuthSources().forEach((k, v) -> { UserProfile profile = v.getUserProfile(username); - List<UserProfile> groupProfiles = v.searchUserProfiles(username, "LOGON_NAME"); + List<UserProfile> groupProfiles = v.searchUserProfiles(username, "LOGON_NAME", Optional.empty()); if (!groupProfiles.isEmpty()) { UserGroupMapping mapping = null; StatementList statementList = new StatementList(); diff --git a/jams-server/src/main/java/net/jami/jams/server/core/workflows/RegisterDeviceFlow.java b/jams-server/src/main/java/net/jami/jams/server/core/workflows/RegisterDeviceFlow.java index 324f1a6dd529de0ddcc7b3af9cb3009cc7782ebe..79461707e5ae7ab0da40d86ab999b176b887b56a 100644 --- a/jams-server/src/main/java/net/jami/jams/server/core/workflows/RegisterDeviceFlow.java +++ b/jams-server/src/main/java/net/jami/jams/server/core/workflows/RegisterDeviceFlow.java @@ -36,6 +36,7 @@ import net.jami.jams.dht.DeviceReceiptGenerator; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import static net.jami.jams.server.Server.certificateAuthority; import static net.jami.jams.server.Server.dataStore; @@ -45,14 +46,15 @@ import static net.jami.jams.server.Server.userAuthenticationModule; @Slf4j public class RegisterDeviceFlow { - public static DeviceRegistrationResponse registerDevice(String username, DeviceRegistrationRequest registrationRequest){ + public static DeviceRegistrationResponse registerDevice(String username, + DeviceRegistrationRequest registrationRequest) { try { StatementList statementList = new StatementList(); statementList.addStatement(new StatementElement("username", "=", username, "")); User user = dataStore.getUserDao().getObjects(statementList).get(0); UserProfile userProfile = userAuthenticationModule.getAuthSources() - .get(new AuthModuleKey(user.getRealm(),user.getUserType())) - .searchUserProfiles(username,"LOGON_NAME").get(0); + .get(new AuthModuleKey(user.getRealm(), user.getUserType())) + .searchUserProfiles(username, "LOGON_NAME", Optional.empty()).get(0); if (user == null) { log.error("Tried to enroll a device, but could not find a user, this is impossible!"); @@ -62,7 +64,7 @@ public class RegisterDeviceFlow { device.setOwner(username); device.setDisplayName(registrationRequest.getDeviceName()); device = certificateAuthority.getSignedCertificate(user, device); - if(device == null){ + if (device == null) { log.error("Could not succesfully create a device certificate!"); return null; } @@ -83,11 +85,9 @@ public class RegisterDeviceFlow { userProfile.setGroupMemberships(list); } - - - //Now we build this response out. + // Now we build this response out. DeviceRegistrationResponse response = new DeviceRegistrationResponse(); - if(userProfile.getGroupMemberships() != null) { + if (userProfile.getGroupMemberships() != null) { userProfile.getGroupMemberships().forEach(e -> { if (!e.equals("")) { StatementElement st = new StatementElement("name", "=", e, ""); @@ -96,7 +96,7 @@ public class RegisterDeviceFlow { Group group = dataStore.getGroupDao().getObjects(statementList1).get(0); String policyName = group.getBlueprint(); - if(group != null && policyName != null){ + if (group != null && policyName != null) { StatementElement st2 = new StatementElement("name", "=", policyName, ""); StatementList statementList2 = new StatementList(); statementList2.addStatement((st2)); @@ -110,21 +110,22 @@ public class RegisterDeviceFlow { } }); } - //We need to set the device receipt.... - String[] devReceipt = DeviceReceiptGenerator.generateReceipt(user.getPrivateKey(), user.getCertificate().getPublicKey(), - device.getCertificate().getPublicKey(), user.getEthAddress()); + // We need to set the device receipt.... + String[] devReceipt = DeviceReceiptGenerator.generateReceipt(user.getPrivateKey(), + user.getCertificate().getPublicKey(), device.getCertificate().getPublicKey(), user.getEthAddress()); response.setDeviceReceipt(devReceipt[0]); response.setReceiptSignature(devReceipt[1]); - response.setDisplayName(userProfile.getFirstName()+" "+userProfile.getLastName()); - //We need to set + response.setDisplayName(userProfile.getFirstName() + " " + userProfile.getLastName()); + // We need to set response.setNameServer(nameServer.getURI()); - if(userProfile.getProfilePicture() != null) response.setUserPhoto(userProfile.getProfilePicture()); - //Finally we set the certificate chain. - response.setCertificateChain(new X509Certificate[]{certificateAuthority.getCA(),user.getCertificate(),device.getCertificate()}); + if (userProfile.getProfilePicture() != null) + response.setUserPhoto(userProfile.getProfilePicture()); + // Finally we set the certificate chain. + response.setCertificateChain(new X509Certificate[] { certificateAuthority.getCA(), user.getCertificate(), + device.getCertificate() }); return response; - } - catch (Exception e){ - log.error("An exception has occurred while trying to enroll a device with error {}",e.getMessage()); + } catch (Exception e) { + log.error("An exception has occurred while trying to enroll a device with error {}", e.getMessage()); return null; } } diff --git a/jams-server/src/main/java/net/jami/jams/server/servlets/api/auth/directory/DirectoryEntryServlet.java b/jams-server/src/main/java/net/jami/jams/server/servlets/api/auth/directory/DirectoryEntryServlet.java index 735bb96aacbdb382a7d8def9747d6d97194f0594..33b5d5bdd92c3f304316e3e6efb4fe320b957c92 100644 --- a/jams-server/src/main/java/net/jami/jams/server/servlets/api/auth/directory/DirectoryEntryServlet.java +++ b/jams-server/src/main/java/net/jami/jams/server/servlets/api/auth/directory/DirectoryEntryServlet.java @@ -38,6 +38,7 @@ import net.jami.jams.common.objects.user.UserProfile; import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import static net.jami.jams.server.Server.dataStore; import static net.jami.jams.server.Server.userAuthenticationModule; @@ -202,7 +203,7 @@ public class DirectoryEntryServlet extends HttpServlet { User user = dataStore.getUserDao().getObjects(statementList).get(0); List<UserProfile> userProfiles = new ArrayList<>(); userAuthenticationModule.getAuthSources().forEach((k, v) -> { - userProfiles.addAll(v.searchUserProfiles(user.getUsername(), "LOGON_NAME")); + userProfiles.addAll(v.searchUserProfiles(user.getUsername(), "LOGON_NAME", Optional.empty())); }); if(req.getParameter("format") != null && req.getParameter("format").equals("vcard")){ resp.getOutputStream().write(userProfiles.get(0).getAsVCard().getBytes()); @@ -213,7 +214,7 @@ public class DirectoryEntryServlet extends HttpServlet { if (req.getParameter("directory") != null && req.getParameter("directoryType") != null) { List<UserProfile> profiles = userAuthenticationModule.getAuthSources() .get(new AuthModuleKey(req.getParameter("directory"), AuthenticationSourceType.fromString(req.getParameter("directoryType")))) - .searchUserProfiles(req.getParameter("username"), "LOGON_NAME"); + .searchUserProfiles(req.getParameter("username"), "LOGON_NAME", Optional.empty()); if(req.getParameter("format") != null && req.getParameter("format").equals("vcard")){ resp.getOutputStream().write(profiles.get(0).getAsVCard().getBytes()); } @@ -222,7 +223,7 @@ public class DirectoryEntryServlet extends HttpServlet { } List<UserProfile> userProfiles = new ArrayList<>(); userAuthenticationModule.getAuthSources().forEach((k, v) -> { - userProfiles.addAll(v.searchUserProfiles(req.getParameter("username"), "LOGON_NAME")); + userProfiles.addAll(v.searchUserProfiles(req.getParameter("username"), "LOGON_NAME", Optional.empty())); }); if(req.getParameter("format") != null && req.getParameter("format").equals("vcard")){ resp.getOutputStream().write(userProfiles.get(0).getAsVCard().getBytes()); diff --git a/jams-server/src/main/java/net/jami/jams/server/servlets/api/auth/directory/SearchDirectoryServlet.java b/jams-server/src/main/java/net/jami/jams/server/servlets/api/auth/directory/SearchDirectoryServlet.java index a8e25006c5c0e1ab2e9983c848daca388c2ab35f..2bdf886074eb56ac155720b56c50af4a3453b3fc 100644 --- a/jams-server/src/main/java/net/jami/jams/server/servlets/api/auth/directory/SearchDirectoryServlet.java +++ b/jams-server/src/main/java/net/jami/jams/server/servlets/api/auth/directory/SearchDirectoryServlet.java @@ -20,7 +20,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */ - package net.jami.jams.server.servlets.api.auth.directory; import com.jsoniter.output.JsonStream; @@ -29,17 +28,21 @@ import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import net.jami.jams.common.annotations.JsonContent; +import net.jami.jams.common.authentication.AuthenticationSource; +import net.jami.jams.common.authentication.AuthenticationSourceType; +import net.jami.jams.common.authmodule.AuthModuleKey; import net.jami.jams.common.dao.StatementElement; import net.jami.jams.common.dao.StatementList; import net.jami.jams.common.objects.user.AccessLevel; import net.jami.jams.common.objects.user.User; import net.jami.jams.common.objects.user.UserGroupMapping; import net.jami.jams.common.objects.user.UserProfile; +import net.jami.jams.server.servlets.api.install.CachedObjects; +import org.json.JSONObject; import java.io.IOException; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; import static net.jami.jams.server.Server.dataStore; import static net.jami.jams.server.Server.nameServer; @@ -47,10 +50,11 @@ import static net.jami.jams.server.Server.userAuthenticationModule; @WebServlet("/api/auth/directory/search") public class SearchDirectoryServlet extends HttpServlet { - - //The search directory function does not automatically create users, this would be costly at this point - //right now, we will implement it when Jami supports lists of users. this is a work in progress as it - //requires changes on the name server as well. + // The search directory function does not automatically create users, this would + // be costly at this point + // right now, we will implement it when Jami supports lists of users. this is a + // work in progress as it + // requires changes on the name server as well. List<UserProfile> userProfiles = new ArrayList<>(); /** @@ -59,23 +63,42 @@ public class SearchDirectoryServlet extends HttpServlet { @Override @JsonContent protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { + Optional<Integer> page; + if (req.getParameter("page") == null) + page = Optional.empty(); + else + page = Optional.ofNullable(Integer.parseInt(req.getParameter("page"))); + + ConcurrentHashMap<AuthModuleKey, AuthenticationSource> authSources = new ConcurrentHashMap<>( + userAuthenticationModule.getAuthSources()); + + if (authSources.size() > 1) { + authSources.forEach((k, v) -> { + if (k.getType() == AuthenticationSourceType.LOCAL) + authSources.remove(k); + }); + } - userAuthenticationModule.getAuthSources().forEach((k, v) -> { + authSources.forEach((k, v) -> { if (req.getParameter("queryString").equals("*")) - userProfiles = v.searchUserProfiles(req.getParameter("queryString"), "FULL_TEXT_NAME"); + userProfiles = v.searchUserProfiles(req.getParameter("queryString"), "FULL_TEXT_NAME", page); else { - userProfiles = v.searchUserProfiles(req.getParameter("queryString"), "FULL_TEXT_NAME"); - List<UserProfile> profiles2 = v.searchUserProfiles(req.getParameter("queryString"), "LOGON_NAME"); - for (Iterator<UserProfile> it = userProfiles.iterator(); it.hasNext(); ) { - UserProfile p = it.next(); - for (UserProfile p2 : profiles2) { - if (p2.equals(p)) - it.remove(); - } + userProfiles = v.searchUserProfiles(req.getParameter("queryString"), "FULL_TEXT_NAME", page); + if (userProfiles.isEmpty() && userProfiles + .addAll(v.searchUserProfiles(req.getParameter("queryString"), "LOGON_NAME", page))) { + Set<UserProfile> s = new TreeSet<UserProfile>(new Comparator<UserProfile>() { + @Override + public int compare(UserProfile o1, UserProfile o2) { + if (o1.getUsername().equals(o2.getUsername())) + return 0; + return 1; + } + }); + s.addAll(userProfiles); + userProfiles = new ArrayList<>(s); } - userProfiles.addAll(profiles2); - } + } userProfiles.parallelStream().forEach(profile -> { StatementList statementList = new StatementList(); StatementElement statementElement = new StatementElement("username", "=", profile.getUsername(), ""); @@ -96,9 +119,14 @@ public class SearchDirectoryServlet extends HttpServlet { } }); }); + + JSONObject obj = new JSONObject(); + obj.put("profiles", userProfiles); + obj.put("numPages", dataStore.NUM_PAGES); if (!userProfiles.isEmpty()) { - resp.getOutputStream().write(JsonStream.serialize(userProfiles).getBytes()); + resp.getOutputStream().write((obj.toString()).getBytes()); resp.setStatus(200); - } else resp.setStatus(204, "No users were found!"); + } else + resp.setStatus(204, "No users were found!"); } -} +} \ No newline at end of file diff --git a/ldap-connector/src/main/java/net/jami/jams/ldap/connector/LDAPConnector.java b/ldap-connector/src/main/java/net/jami/jams/ldap/connector/LDAPConnector.java index 8179ed2192ceec1c03f2f500ce368dc48715e9b6..4e3902b903dde972d8e18bab944d1d2ed2d76090 100644 --- a/ldap-connector/src/main/java/net/jami/jams/ldap/connector/LDAPConnector.java +++ b/ldap-connector/src/main/java/net/jami/jams/ldap/connector/LDAPConnector.java @@ -39,6 +39,7 @@ import org.ldaptive.Credential; import org.ldaptive.DefaultConnectionFactory; import java.util.List; +import java.util.Optional; @Slf4j public class LDAPConnector implements AuthenticationSource { @@ -69,13 +70,13 @@ public class LDAPConnector implements AuthenticationSource { } @Override - public List<UserProfile> searchUserProfiles(String queryString, String field) { - return userProfileService.getUserProfile(queryString,field,false); + public List<UserProfile> searchUserProfiles(String queryString, String field, Optional<Integer> page) { + return userProfileService.getUserProfile(queryString,field,false, page); } @Override public UserProfile getUserProfile(String username) { - List<UserProfile> results = userProfileService.getUserProfile(username,"LOGON_NAME",true); + List<UserProfile> results = userProfileService.getUserProfile(username,"LOGON_NAME",true, Optional.empty()); if(results == null || results.size() != 1) return null; return results.get(0); } @@ -98,7 +99,7 @@ public class LDAPConnector implements AuthenticationSource { @Override public boolean test() { - return (searchUserProfiles("*","LOGON_NAME").size() != 0); + return (searchUserProfiles("*","LOGON_NAME", Optional.empty()).size() != 0); } diff --git a/ldap-connector/src/main/java/net/jami/jams/ldap/connector/service/UserProfileService.java b/ldap-connector/src/main/java/net/jami/jams/ldap/connector/service/UserProfileService.java index 29e026f063ca552d6fbe36f72a44be7fb93badc6..aeb8e5a1dcbd88d366f2dab15f264de001aded2d 100644 --- a/ldap-connector/src/main/java/net/jami/jams/ldap/connector/service/UserProfileService.java +++ b/ldap-connector/src/main/java/net/jami/jams/ldap/connector/service/UserProfileService.java @@ -34,9 +34,7 @@ import org.ldaptive.SearchOperation; import org.ldaptive.SearchRequest; import org.ldaptive.SearchResponse; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; +import java.util.*; import java.util.stream.Collectors; import static net.jami.jams.server.Server.dataStore; @@ -49,7 +47,7 @@ public class UserProfileService { this.connectionFactory = connectionFactory; } - public List<UserProfile> getUserProfile(String queryString, String field, boolean exactMatch){ + public List<UserProfile> getUserProfile(String queryString, String field, boolean exactMatch, Optional<Integer> page){ Connection connection = null; try { queryString = queryString.replaceAll("[^\\x00-\\x7F]","*"); @@ -58,10 +56,24 @@ public class UserProfileService { connection.open(); SearchOperation search = new SearchOperation(connectionFactory); SearchResponse res = search.execute(buildRequest(queryString,field, exactMatch)); + + dataStore.NUM_PAGES = (Integer) res.getEntries().size() / dataStore.RESULTS_PER_PAGE; + if (res.getEntries().size() % dataStore.RESULTS_PER_PAGE != 0) + dataStore.NUM_PAGES++; + + if (page.isPresent() && !res.getEntries().isEmpty()) { + if (res.getEntries().size() < dataStore.RESULTS_PER_PAGE) + res= res.subResult(0, res.getEntries().size()); + else if (page.get() * dataStore.RESULTS_PER_PAGE > res.getEntries().size()) + res = res.subResult((page.get()-1) * dataStore.RESULTS_PER_PAGE, res.getEntries().size()); + else + res = res.subResult((page.get()-1) * dataStore.RESULTS_PER_PAGE, (page.get() * dataStore.RESULTS_PER_PAGE)); + + } + if (res.getEntries().size() == 0) return new ArrayList<>(); List<UserProfile> profilesFromResponse = res.getEntries().stream().map(UserProfileService::profileFromResponse).collect(Collectors.toList()); for (UserProfile p: profilesFromResponse) { - StatementList statementList = new StatementList(); StatementElement st = new StatementElement("username", "=", p.getUsername(), ""); statementList.addStatement(st); diff --git a/ldap-connector/src/test/java/tests/GenericLDAPTest.java b/ldap-connector/src/test/java/tests/GenericLDAPTest.java index 82cb05e5146f6428835e6a427b8c8fd81663258f..cb5698460b136c38d586cc82fc4664eafc7b700a 100644 --- a/ldap-connector/src/test/java/tests/GenericLDAPTest.java +++ b/ldap-connector/src/test/java/tests/GenericLDAPTest.java @@ -11,6 +11,7 @@ import org.zapodot.junit.ldap.EmbeddedLdapRuleBuilder; import java.io.InputStream; import java.util.List; +import java.util.Optional; public class GenericLDAPTest { @@ -34,7 +35,7 @@ public class GenericLDAPTest { @Test public void testLookUp() throws Exception{ initLdapConnector(); - List<UserProfile> profiles = ldapConnector.searchUserProfiles("*","FULL_TEXT_NAME"); + List<UserProfile> profiles = ldapConnector.searchUserProfiles("*","FULL_TEXT_NAME", Optional.empty()); Assertions.assertEquals(2,profiles.size()); Assertions.assertNotNull(profiles.get(0).getFirstName()); Assertions.assertNotNull(profiles.get(1).getFirstName()); @@ -53,7 +54,7 @@ public class GenericLDAPTest { @Test public void getVcard() throws Exception{ initLdapConnector(); - List<UserProfile> profiles = ldapConnector.searchUserProfiles("Felix","FULL_TEXT_NAME"); + List<UserProfile> profiles = ldapConnector.searchUserProfiles("Felix","FULL_TEXT_NAME", Optional.empty()); Assert.assertEquals(1,profiles.size()); Assert.assertNotNull(profiles.get(0).getUsername()); String vcard = profiles.get(0).getAsVCard();