source: AnnotationManagerN4J/src/main/java/de/mpiwg/itgroup/annotations/restlet/AnnotatorResourceImpl.java @ 14:629e15b345aa

Last change on this file since 14:629e15b345aa was 14:629e15b345aa, checked in by casties, 12 years ago

permissions mostly work. need more server-side checking.

File size: 20.3 KB
Line 
1/**
2 * Base class for Annotator resource classes.
3 */
4package de.mpiwg.itgroup.annotations.restlet;
5
6import java.io.UnsupportedEncodingException;
7import java.security.InvalidKeyException;
8import java.security.SignatureException;
9import java.text.SimpleDateFormat;
10import java.util.ArrayList;
11import java.util.Calendar;
12import java.util.List;
13import java.util.regex.Matcher;
14import java.util.regex.Pattern;
15
16import javax.servlet.ServletContext;
17
18import net.oauth.jsontoken.Checker;
19import net.oauth.jsontoken.JsonToken;
20import net.oauth.jsontoken.JsonTokenParser;
21import net.oauth.jsontoken.SystemClock;
22import net.oauth.jsontoken.crypto.HmacSHA256Verifier;
23import net.oauth.jsontoken.crypto.Verifier;
24
25import org.apache.commons.codec.binary.Base64;
26import org.apache.log4j.Logger;
27import org.json.JSONArray;
28import org.json.JSONException;
29import org.json.JSONObject;
30import org.restlet.data.Form;
31import org.restlet.data.Status;
32import org.restlet.representation.Representation;
33import org.restlet.resource.Options;
34import org.restlet.resource.ServerResource;
35
36import de.mpiwg.itgroup.annotations.Actor;
37import de.mpiwg.itgroup.annotations.Annotation;
38import de.mpiwg.itgroup.annotations.Annotation.FragmentTypes;
39import de.mpiwg.itgroup.annotations.Group;
40import de.mpiwg.itgroup.annotations.Person;
41import de.mpiwg.itgroup.annotations.neo4j.AnnotationStore;
42import de.mpiwg.itgroup.annotations.old.NS;
43
44/**
45 * Base class for Annotator resource classes.
46 *
47 * @author dwinter, casties
48 *
49 */
50public abstract class AnnotatorResourceImpl extends ServerResource {
51
52    protected static Logger logger = Logger.getLogger(AnnotatorResourceImpl.class);
53
54    private AnnotationStore store;
55
56    protected String getAllowedMethodsForHeader() {
57        return "OPTIONS,GET,POST";
58    }
59
60    protected AnnotationStore getAnnotationStore() {
61        if (store == null) {
62            ServletContext sc = (ServletContext) getContext().getServerDispatcher().getContext().getAttributes()
63                    .get("org.restlet.ext.servlet.ServletContext");
64            logger.debug("Getting AnnotationStore from Context");
65            store = (AnnotationStore) sc.getAttribute(RestServer.ANNSTORE_KEY);
66        }
67        return store;
68    }
69
70    public String encodeJsonId(String id) {
71        try {
72            return Base64.encodeBase64URLSafeString(id.getBytes("UTF-8"));
73        } catch (UnsupportedEncodingException e) {
74            return null;
75        }
76    }
77
78    public String decodeJsonId(String id) {
79        try {
80            return new String(Base64.decodeBase64(id), "UTF-8");
81        } catch (UnsupportedEncodingException e) {
82            return null;
83        }
84    }
85
86    /**
87     * Handle options request to allow CORS for AJAX.
88     *
89     * @param entity
90     */
91    @Options
92    public void doOptions(Representation entity) {
93        logger.debug("AnnotatorResourceImpl doOptions!");
94        setCorsHeaders();
95    }
96
97    /**
98     * set headers to allow CORS for AJAX.
99     */
100    protected void setCorsHeaders() {
101        Form responseHeaders = (Form) getResponse().getAttributes().get("org.restlet.http.headers");
102        if (responseHeaders == null) {
103            responseHeaders = new Form();
104            getResponse().getAttributes().put("org.restlet.http.headers", responseHeaders);
105        }
106        responseHeaders.add("Access-Control-Allow-Methods", getAllowedMethodsForHeader());
107        // echo back Origin and Request-Headers
108        Form requestHeaders = (Form) getRequest().getAttributes().get("org.restlet.http.headers");
109        String origin = requestHeaders.getFirstValue("Origin", true);
110        if (origin == null) {
111            responseHeaders.add("Access-Control-Allow-Origin", "*");
112        } else {
113            responseHeaders.add("Access-Control-Allow-Origin", origin);
114        }
115        String allowHeaders = requestHeaders.getFirstValue("Access-Control-Request-Headers", true);
116        if (allowHeaders != null) {
117            responseHeaders.add("Access-Control-Allow-Headers", allowHeaders);
118        }
119        responseHeaders.add("Access-Control-Allow-Credentials", "true");
120        responseHeaders.add("Access-Control-Max-Age", "60");
121    }
122
123    /**
124     * returns if authentication information from headers is valid.
125     *
126     * @param entity
127     * @return
128     */
129    public boolean isAuthenticated(Representation entity) {
130        return (checkAuthToken(entity) != null);
131    }
132
133    /**
134     * checks Annotator Auth plugin authentication information from headers.
135     * returns userId if successful.
136     *
137     * @param entity
138     * @return
139     */
140    public String checkAuthToken(Representation entity) {
141        Form requestHeaders = (Form) getRequest().getAttributes().get("org.restlet.http.headers");
142        String authToken = requestHeaders.getFirstValue("x-annotator-auth-token", true);
143        if (authToken == null) return null;
144        // decode token first to get consumer key
145        JsonToken token = new JsonTokenParser(null, null).deserialize(authToken);
146        String userId = token.getParamAsPrimitive("userId").getAsString();
147        String consumerKey = token.getParamAsPrimitive("consumerKey").getAsString();
148        // get stored consumer secret for key
149        RestServer restServer = (RestServer) getApplication();
150        String consumerSecret = restServer.getConsumerSecret(consumerKey);
151        logger.debug("requested consumer key=" + consumerKey + " secret=" + consumerSecret);
152        if (consumerSecret == null) {
153            return null;
154        }
155        // logger.debug(String.format("token=%s tokenString=%s signatureAlgorithm=%s",token,token.getTokenString(),token.getSignatureAlgorithm()));
156        try {
157            List<Verifier> verifiers = new ArrayList<Verifier>();
158            // we only do HS256 yet
159            verifiers.add(new HmacSHA256Verifier(consumerSecret.getBytes("UTF-8")));
160            // verify token signature(should really be static...)
161            new JsonTokenParser(new SystemClock(), null, (Checker[]) null).verify(token, verifiers);
162        } catch (SignatureException e) {
163            // TODO Auto-generated catch block
164            e.printStackTrace();
165        } catch (InvalidKeyException e) {
166            // TODO Auto-generated catch block
167            e.printStackTrace();
168        } catch (UnsupportedEncodingException e) {
169            // TODO Auto-generated catch block
170            e.printStackTrace();
171        }
172        // must be ok then
173        logger.debug("auth OK! user=" + userId);
174        return userId;
175    }
176
177    /**
178     * creates Annotator-JSON from an Annotation object.
179     *
180     * @param annot
181     * @param forAnonymous TODO
182     * @return
183     */
184    public JSONObject createAnnotatorJson(Annotation annot, boolean forAnonymous) {
185        // return user as a JSON object (otherwise just as string)
186        boolean makeUserObject = true;
187        JSONObject jo = new JSONObject();
188        try {
189            jo.put("text", annot.getBodyText());
190            jo.put("uri", annot.getTargetBaseUri());
191
192            if (makeUserObject) {
193                // create user object
194                JSONObject userObject = new JSONObject();
195                Actor creator = annot.getCreator();
196                // save creator as uri
197                userObject.put("uri", creator.getUri());
198                // make short user id
199                String userId = creator.getIdString();
200                // set as id
201                userObject.put("id", userId);
202                // get full name
203                String userName = creator.getName();
204                if (userName == null) {
205                    RestServer restServer = (RestServer) getApplication();
206                    userName = restServer.getFullNameFromLdap(userId);
207                }
208                userObject.put("name", userName);
209                // save user object
210                jo.put("user", userObject);
211            } else {
212                // save user as string
213                jo.put("user", annot.getCreatorUri());
214            }
215
216            if (annot.getTargetFragment() != null) {
217                // we only look at the first xpointer
218                List<String> fragments = new ArrayList<String>();
219                fragments.add(annot.getTargetFragment());
220                FragmentTypes xt = annot.getFragmentType();
221                if (xt == FragmentTypes.XPOINTER) {
222                    jo.put("ranges", transformToRanges(fragments));
223                } else if (xt == FragmentTypes.AREA) {
224                    jo.put("areas", transformToAreas(fragments));
225                }
226            }
227           
228            // permissions
229            JSONObject perms = new JSONObject();
230            jo.put("permissions", perms);
231            // admin
232            JSONArray adminPerms = new JSONArray();
233            perms.put("admin", adminPerms);
234            Actor adminPerm = annot.getAdminPermission();
235            if (adminPerm != null) {
236                adminPerms.put(adminPerm.getIdString());
237            } else if (forAnonymous) {
238                // set something because its not allowed for anonymous
239                adminPerms.put("not-you");
240            }
241            // delete
242            JSONArray deletePerms = new JSONArray();
243            perms.put("delete", deletePerms);
244            Actor deletePerm = annot.getDeletePermission();
245            if (deletePerm != null) {
246                deletePerms.put(deletePerm.getIdString());
247            } else if (forAnonymous) {
248                // set something because its not allowed for anonymous
249                deletePerms.put("not-you");
250            }
251            // update
252            JSONArray updatePerms = new JSONArray();
253            perms.put("update", updatePerms);
254            Actor updatePerm = annot.getUpdatePermission();
255            if (updatePerm != null) {
256                updatePerms.put(updatePerm.getIdString());
257            } else if (forAnonymous) {
258                // set something because its not allowed for anonymous
259                updatePerms.put("not-you");
260            }
261            // read
262            JSONArray readPerms = new JSONArray();
263            perms.put("read", readPerms);
264            Actor readPerm = annot.getReadPermission();
265            if (readPerm != null) {
266                readPerms.put(readPerm.getIdString());
267            }
268           
269            // encode Annotation URL (=id) in base64
270            String annotUrl = annot.getUri();
271            String annotId = encodeJsonId(annotUrl);
272            jo.put("id", annotId);
273            return jo;
274        } catch (JSONException e) {
275            // TODO Auto-generated catch block
276            e.printStackTrace();
277        }
278        return null;
279    }
280
281    private JSONArray transformToRanges(List<String> xpointers) {
282
283        JSONArray ja = new JSONArray();
284
285        Pattern rg = Pattern
286                .compile("xpointer\\(start-point\\(string-range\\(\"([^\"]*)\",([^,]*),1\\)\\)/range-to\\(end-point\\(string-range\\(\"([^\"]*)\",([^,]*),1\\)\\)\\)\\)");
287        Pattern rg1 = Pattern.compile("xpointer\\(start-point\\(string-range\\(\"([^\"]*)\",([^,]*),1\\)\\)\\)");
288
289        try {
290            for (String xpointer : xpointers) {
291                // String decoded = URLDecoder.decode(xpointer, "utf-8");
292                String decoded = xpointer;
293                Matcher m = rg.matcher(decoded);
294
295                if (m.find()) {
296                    {
297                        JSONObject jo = new JSONObject();
298                        jo.put("start", m.group(1));
299                        jo.put("startOffset", m.group(2));
300                        jo.put("end", m.group(3));
301                        jo.put("endOffset", m.group(4));
302                        ja.put(jo);
303                    }
304                }
305                m = rg1.matcher(xpointer);
306                if (m.find()) {
307                    JSONObject jo = new JSONObject();
308                    jo.put("start", m.group(1));
309                    jo.put("startOffset", m.group(2));
310
311                    ja.put(jo);
312                }
313            }
314        } catch (JSONException e) {
315            // TODO Auto-generated catch block
316            e.printStackTrace();
317        }
318        return ja;
319    }
320
321    private JSONArray transformToAreas(List<String> xpointers) {
322
323        JSONArray ja = new JSONArray();
324
325        Pattern rg = Pattern.compile("xywh=(\\w*:)([\\d\\.]+),([\\d\\.]+),([\\d\\.]+),([\\d\\.]+)");
326
327        try {
328            for (String xpointer : xpointers) {
329                // String decoded = URLDecoder.decode(xpointer, "utf-8");
330                String decoded = xpointer;
331                Matcher m = rg.matcher(decoded);
332
333                if (m.find()) {
334                    {
335                        JSONObject jo = new JSONObject();
336                        @SuppressWarnings("unused")
337                        String unit = m.group(1);
338                        jo.put("x", m.group(2));
339                        jo.put("y", m.group(3));
340                        jo.put("width", m.group(4));
341                        jo.put("height", m.group(5));
342                        ja.put(jo);
343                    }
344                }
345            }
346        } catch (JSONException e) {
347            // TODO Auto-generated catch block
348            e.printStackTrace();
349        }
350        return ja;
351    }
352
353    protected String parseArea(JSONObject area) throws JSONException {
354        String x = area.getString("x");
355        String y = area.getString("y");
356        String width = "0";
357        String height = "0";
358        if (area.has("width")) {
359            width = area.getString("width");
360            height = area.getString("height");
361        }
362        String fragment = String.format("xywh=fraction:%s,%s,%s,%s", x, y, width, height);
363        return fragment;
364    }
365
366    protected String parseRange(JSONObject range) throws JSONException {
367        String start = range.getString("start");
368        String end = range.getString("end");
369        String startOffset = range.getString("startOffset");
370        String endOffset = range.getString("endOffset");
371
372        String fragment = String.format(
373                "xpointer(start-point(string-range(\"%s\",%s,1))/range-to(end-point(string-range(\"%s\",%s,1))))", start,
374                startOffset, end, endOffset);
375        return fragment;
376    }
377
378    /**
379     * Creates an Annotation object with data from JSON.
380     *
381     * uses the specification from the annotator project: {@link https
382     * ://github.com/okfn/annotator/wiki/Annotation-format}
383     *
384     * The username will be transformed to an URI if not given already as URI,
385     * if not it will set to the MPIWG namespace defined in
386     * de.mpiwg.itgroup.annotationManager.Constants.NS
387     *
388     * @param jo
389     * @return
390     * @throws JSONException
391     * @throws UnsupportedEncodingException
392     */
393    public Annotation createAnnotation(JSONObject jo, Representation entity) throws JSONException, UnsupportedEncodingException {
394        return updateAnnotation(new Annotation(), jo, entity);
395    }
396
397    /**
398     * Updates an Annotation object with data from JSON.
399     *
400     * uses the specification from the annotator project: {@link https
401     * ://github.com/okfn/annotator/wiki/Annotation-format}
402     *
403     * The username will be transformed to an URI if not given already as URI,
404     * if not it will set to the MPIWG namespace defined in
405     * de.mpiwg.itgroup.annotationManager.Constants.NS
406     *
407     * @param annot
408     * @param jo
409     * @return
410     * @throws JSONException
411     * @throws UnsupportedEncodingException
412     */
413    public Annotation updateAnnotation(Annotation annot, JSONObject jo, Representation entity) throws JSONException,
414            UnsupportedEncodingException {
415        // target uri
416        if (jo.has("uri")) {
417            annot.setTargetBaseUri(jo.getString("uri"));
418        }
419        // annotation text
420        if (jo.has("text")) {
421            annot.setBodyText(jo.getString("text"));
422        }
423        // check authentication
424        String authUser = checkAuthToken(entity);
425        if (authUser == null) {
426            /*
427             * // try http auth User httpUser = getHttpAuthUser(entity); if
428             * (httpUser == null) {
429             */
430            setStatus(Status.CLIENT_ERROR_FORBIDDEN);
431            return null;
432            /*
433             * } authUser = httpUser.getIdentifier();
434             */
435        }
436        // get or create creator object
437        Actor creator = annot.getCreator();
438        if (creator == null) {
439            creator = new Person();
440            annot.setCreator(creator);
441        }
442        // username not required, if no username given authuser will be used
443        String username = null;
444        String userUri = creator.getUri();
445        if (jo.has("user")) {
446            if (jo.get("user") instanceof String) {
447                // user is just a String
448                username = jo.getString("user");
449                creator.setId(username);
450                // TODO: what if username and authUser are different?
451            } else {
452                // user is an object
453                JSONObject user = jo.getJSONObject("user");
454                if (user.has("id")) {
455                    String id = user.getString("id");
456                    creator.setId(id);
457                    username = id;
458                }
459                if (user.has("uri")) {
460                    userUri = user.getString("uri");
461                }
462            }
463        }
464        if (username == null) {
465            username = authUser;
466        }
467        // try to get full name
468        if (creator.getName() == null && username != null) {
469            RestServer restServer = (RestServer) getApplication();
470            String fullName = restServer.getFullNameFromLdap(username);
471            creator.setName(fullName);
472        }
473        // userUri should be a URI, if not it will set to the MPIWG namespace
474        if (userUri == null) {
475            if (username.startsWith("http")) {
476                userUri = username;
477            } else {
478                userUri = NS.MPIWG_PERSONS_URL + username;
479            }
480        }
481        // TODO: should we overwrite the creator?
482        if (creator.getUri() == null) {
483            creator.setUri(userUri);
484        }
485
486        if (annot.getCreated() == null) {
487            // set creation date
488            SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
489            String ct = format.format(Calendar.getInstance().getTime());
490            annot.setCreated(ct);
491        }
492
493        // create xpointer from the first range/area
494        if (jo.has("ranges")) {
495            JSONObject ranges = jo.getJSONArray("ranges").getJSONObject(0);
496            annot.setFragmentType(FragmentTypes.XPOINTER);
497            String fragment = parseRange(ranges);
498            annot.setTargetFragment(fragment);
499        }
500        if (jo.has("areas")) {
501            JSONObject area = jo.getJSONArray("areas").getJSONObject(0);
502            annot.setFragmentType(FragmentTypes.AREA);
503            String fragment = parseArea(area);
504            annot.setTargetFragment(fragment);
505        }
506
507        // permissions
508        if (jo.has("permissions")) {
509            JSONObject permissions = jo.getJSONObject("permissions");
510            if (permissions.has("admin")) {
511                JSONArray perms = permissions.getJSONArray("admin");
512                Actor actor = getActorFromPermissions(perms);
513                annot.setAdminPermission(actor);
514            }
515            if (permissions.has("delete")) {
516                JSONArray perms = permissions.getJSONArray("delete");
517                Actor actor = getActorFromPermissions(perms);
518                annot.setDeletePermission(actor);
519            }
520            if (permissions.has("update")) {
521                JSONArray perms = permissions.getJSONArray("update");
522                Actor actor = getActorFromPermissions(perms);
523                annot.setUpdatePermission(actor);
524            }
525            if (permissions.has("read")) {
526                JSONArray perms = permissions.getJSONArray("read");
527                Actor actor = getActorFromPermissions(perms);
528                annot.setReadPermission(actor);
529            }
530        }
531
532        return annot;
533    }
534
535    @SuppressWarnings("unused")
536    protected Actor getActorFromPermissions(JSONArray perms) throws JSONException {
537        Actor actor = null;
538        for (int i = 0; i < perms.length(); ++i) {
539            String perm = perms.getString(i);
540            if (perm.toLowerCase().startsWith("group:")) {
541                String groupId = perm.substring(6);
542                actor = new Group(groupId);
543            } else {
544                actor = new Person(perm);
545            }
546            // we just take the first one
547            break;
548        }
549        return actor;
550    }
551
552}
Note: See TracBrowser for help on using the repository browser.