001package com.pingidentity.util;
002
003import com.unboundid.ldap.sdk.Entry;
004import org.apache.http.HttpEntity;
005import org.apache.http.HttpResponse;
006import org.apache.http.NameValuePair;
007import org.apache.http.client.HttpClient;
008import org.apache.http.client.entity.UrlEncodedFormEntity;
009import org.apache.http.client.methods.HttpGet;
010import org.apache.http.client.methods.HttpPost;
011import org.apache.http.client.utils.URIBuilder;
012import org.apache.http.impl.client.HttpClients;
013import org.apache.http.message.BasicNameValuePair;
014import org.apache.http.util.EntityUtils;
015import org.json.JSONArray;
016import org.json.JSONException;
017import org.json.JSONObject;
018
019import java.net.URI;
020import java.util.ArrayList;
021import java.util.Iterator;
022import java.util.List;
023import java.util.UUID;
024
025public class MSGraphAPI {
026
027    private static String apiVersion = "1.6";
028
029    private String tenantId = null;
030    private String clientId = null;
031    private String clientSecret = null;
032    private String accessToken = null;
033    private String baseUrl = null;
034
035    public enum OBJECT_TYPE {USERS, GROUPS};
036
037    private HttpClient httpclient = HttpClients.createDefault();
038
039    // TODO Needs to figure out throttling with AzureAD GraphAPI.
040    // TODO Maybe allow for user to specify a throttle as an extension argument.
041
042    /**
043     * Get and cache an accessToken
044     *
045     * @param tenantId
046     * @param clientId
047     * @param clientSecret
048     */
049    public MSGraphAPI(final String tenantId,
050                      final String clientId,
051                      final String clientSecret) {
052        this.tenantId = tenantId;
053        this.clientId = clientId;
054        this.clientSecret = clientSecret;
055        authenticate();
056
057        baseUrl = "https://graph.windows.net/" + tenantId + "/";
058    }
059
060
061    /**
062     * Authenticate to microsoft using the OAuth2 Client Credentials Grant Flow
063     * <p>
064     * https://docs.microsoft.com/en-us/azure/active-directory/develop/v1-oauth2-client-creds-grant-flow
065     */
066    private void authenticate() {
067        HttpClient httpclient = HttpClients.createDefault();
068
069        try {
070            URIBuilder builder = new URIBuilder("https://login.microsoftonline.com/" + tenantId + "/oauth2/token");
071            // Specify values for the following required parameters
072            builder.setParameter("api-version", "1.0");
073            URI uri = builder.build();
074            HttpPost request = new HttpPost(uri);
075
076            // Specify required body to authenticate
077            List<NameValuePair> postParameters = new ArrayList<>();
078
079            postParameters.add(new BasicNameValuePair("client_id", clientId));
080            postParameters.add(new BasicNameValuePair("client_secret", clientSecret));
081            postParameters.add(new BasicNameValuePair("grant_type", "client_credentials"));
082            postParameters.add(new BasicNameValuePair("resource", "https://graph.windows.net"));
083
084            request.setEntity(new UrlEncodedFormEntity(postParameters, "UTF-8"));
085
086            HttpResponse response = httpclient.execute(request);
087            HttpEntity entity = response.getEntity();
088            if (entity != null) {
089                JSONObject json = new JSONObject(EntityUtils.toString(entity));
090
091                accessToken = json.getString("access_token");
092
093                //System.out.println(accessToken);
094            }
095        } catch (Exception e) {
096            System.out.println(e.getMessage());
097        }
098    }
099
100
101    /**
102     * Get an entry basedon objectType and objectId
103     *
104     * @param objType
105     * @param objectId
106     * @return
107     */
108    public Entry getEntry(final OBJECT_TYPE objType,
109                          final UUID objectId) {
110        return getEntry(objType, objectId, null);
111    }
112
113    /**
114     * Get an etnry based on objectType and objectId.  Also return specific attributes
115     *
116     * @param objType
117     * @param objectId
118     * @param attributeNames
119     * @return
120     */
121    public Entry getEntry(final OBJECT_TYPE objType,
122                          final UUID objectId,
123                          final String attributeNames) {
124        MSGraphResult graphResult = getEntries(objType, objectId,
125                null, attributeNames);
126
127        List<Entry> entries = graphResult.entries;
128
129        if (entries != null && entries.size() == 1) {
130            return entries.get(0);
131        } else {
132            return null;
133        }
134    }
135
136    /**
137     * Get all entries for a particular objectType
138     *
139     * @param objType
140     * @return
141     */
142    public MSGraphResult getEntries(final OBJECT_TYPE objType) {
143        return getEntries(objType, null, null, null);
144    }
145
146    /**
147     * Get entries for an objectType based on filter returning certain attributes
148     *
149     * @param objType
150     * @param filter
151     * @param attributeNames
152     * @return
153     */
154    public MSGraphResult getEntries(final OBJECT_TYPE objType,
155                                    final String filter,
156                                    final String attributeNames) {
157        return getEntries(objType, null, filter, attributeNames);
158    }
159
160    /**
161     * Get a list of entries based on objectType and filter.  Return a list of attributeNames.
162     *
163     * @param objType
164     * @param filter
165     * @param attributeNames
166     * @return
167     */
168    private MSGraphResult getEntries(final OBJECT_TYPE objType,
169                                     final UUID objectId,
170                                     final String filter,
171                                     final String attributeNames) {
172        List<BasicNameValuePair> bnvp = new ArrayList<>();
173
174        if (filter != null) {
175            bnvp.add(new BasicNameValuePair("$filter", filter));
176        }
177
178        if (attributeNames != null) {
179            bnvp.add(new BasicNameValuePair("$select", attributeNames));
180        }
181
182        return getEntries(objType, objectId, bnvp);
183    }
184
185    /**
186     * Get a list of entries based on objectType.
187     *
188     * @param objType
189     * @param objectId
190     * @param nvPairs
191     * @return
192     */
193    private MSGraphResult getEntries(final OBJECT_TYPE objType,
194                                     final UUID objectId,
195                                     final List<BasicNameValuePair> nvPairs) {
196        MSGraphResult graphResult = new MSGraphResult();
197        List<Entry> objList = new ArrayList<>();
198
199        try {
200            String objectIdURL = "";
201
202            if (objectId != null) {
203                objectIdURL = "/" + objectId;
204            }
205
206            // Specify values for path parameters (shown as {...})
207            URIBuilder builder = new URIBuilder( baseUrl +
208                    objType.toString().toLowerCase() + objectIdURL);
209
210            List<NameValuePair> parameters = new ArrayList<>();
211
212            if (nvPairs != null) {
213                for (NameValuePair nvp : nvPairs) {
214                    parameters.add(nvp);
215                }
216            }
217
218            builder.setParameters(parameters);
219
220            graphResult = getEntries(objType, builder);
221        } catch (Exception e) {
222            System.out.println(e.getMessage());
223        }
224
225        return graphResult;
226    }
227
228    /**
229     * Get the entries based on the objType and builder URL
230     *
231     * @param objType
232     * @param builder
233     * @return
234     */
235    private MSGraphResult getEntries(final OBJECT_TYPE objType,
236                                     final URIBuilder builder) {
237        MSGraphResult graphResult = new MSGraphResult();
238        List<Entry> objList = new ArrayList<>();
239        String nextLink = null;
240
241        try {
242            addVersionParameter(builder);
243            URI uri = builder.build();
244
245            // System.out.println("GETTING: " + uri.toString());
246            HttpGet request = new HttpGet(uri);
247
248            request.addHeader("Authorization", "Bearer " + accessToken);
249            HttpResponse response = httpclient.execute(request);
250            HttpEntity entity = response.getEntity();
251
252            if (entity != null) {
253                String rawString = EntityUtils.toString(entity);
254                // System.out.println("RAW = " + rawString);
255                JSONObject json = new JSONObject(rawString);
256
257                if (json.has("odata.nextLink")) {
258                    nextLink = json.getString("odata.nextLink");
259                }
260
261                if (json.has("value")) {
262                    JSONArray jArray = json.getJSONArray("value");
263
264                    if (jArray != null) {
265                        for (int i = 0; i < jArray.length(); i++) {
266                            Entry entry = getEntryFromJson(objType, jArray.getJSONObject(i));
267                            objList.add(entry);
268                        }
269                    }
270                } else {
271                    Entry entry = getEntryFromJson(objType, json);
272                    objList.add(entry);
273                }
274            }
275
276            graphResult = new MSGraphResult(objType, objList, nextLink);
277        } catch (Exception e) {
278            System.out.println(e.getMessage());
279        }
280
281        return graphResult;
282    }
283
284    /**
285     * Get the next page of results
286     *
287     * @param nextResult
288     * @return
289     */
290    public MSGraphResult nextPage(final MSGraphResult nextResult) {
291        MSGraphResult graphResult = new MSGraphResult();
292
293        if (nextResult.nextLink == null) {
294            return new MSGraphResult();
295        }
296
297        try {
298
299            URIBuilder builder = new URIBuilder(baseUrl + nextResult.nextLink);
300
301            graphResult = getEntries(nextResult.objectType, builder);
302        } catch (Exception e) {
303            System.out.println(e.getMessage());
304        }
305        return graphResult;
306    }
307
308    private void addVersionParameter (URIBuilder builder) {
309        builder.addParameter("api-version", apiVersion);
310    }
311
312    /**
313     * Convert a json entry to an LDAP Entry.
314     * <p>
315     * Expects an objectId to be returned in the JSON.
316     *
317     * @param json
318     * @return
319     */
320    private static Entry getEntryFromJson(final OBJECT_TYPE objectType,
321                                          final JSONObject json) {
322
323        Entry entry = null;
324        try {
325            Iterator<?> keys = json.keys();
326
327            UUID objectId = UUID.fromString(json.getString("objectId"));
328
329            entry = new Entry("cn=" + objectId + ",cn=" + objectType.toString().toLowerCase());
330
331            while (keys.hasNext()) {
332                String key = (String) keys.next();
333
334                if (!key.startsWith("odata.") && !json.isNull(key)) {
335                    Object objVal = json.get(key);
336                    String value = null;
337
338                    if (objVal instanceof JSONObject) {
339                        value = json.toString();
340                    } else if (objVal instanceof JSONArray) {
341
342                    } else if (objVal instanceof Boolean) {
343                        value = new Boolean(json.getBoolean(key)).toString();
344                    } else {
345                        value = json.getString(key);
346                    }
347
348                    if (value != null && !value.isEmpty()) {
349                        entry.addAttribute(key, value);
350                    }
351                }
352            }
353        } catch (JSONException je) {
354            je.printStackTrace();
355        }
356
357        //System.out.println(entry.toLDIFString());
358        return entry;
359    }
360}