001    package de.deepamehta.plugins.accesscontrol;
002    
003    import de.deepamehta.plugins.accesscontrol.event.PostLoginUserListener;
004    import de.deepamehta.plugins.accesscontrol.event.PostLogoutUserListener;
005    import de.deepamehta.plugins.accesscontrol.service.AccessControlService;
006    import de.deepamehta.plugins.workspaces.service.WorkspacesService;
007    
008    import de.deepamehta.core.Association;
009    import de.deepamehta.core.AssociationType;
010    import de.deepamehta.core.ChildTopics;
011    import de.deepamehta.core.DeepaMehtaObject;
012    import de.deepamehta.core.RelatedTopic;
013    import de.deepamehta.core.Topic;
014    import de.deepamehta.core.TopicType;
015    import de.deepamehta.core.Type;
016    import de.deepamehta.core.ViewConfiguration;
017    import de.deepamehta.core.model.AssociationModel;
018    import de.deepamehta.core.model.ChildTopicsModel;
019    import de.deepamehta.core.model.SimpleValue;
020    import de.deepamehta.core.model.TopicModel;
021    import de.deepamehta.core.model.TopicRoleModel;
022    import de.deepamehta.core.osgi.PluginActivator;
023    import de.deepamehta.core.service.DeepaMehtaEvent;
024    import de.deepamehta.core.service.EventListener;
025    import de.deepamehta.core.service.Inject;
026    import de.deepamehta.core.service.Transactional;
027    import de.deepamehta.core.service.accesscontrol.AccessControlException;
028    import de.deepamehta.core.service.accesscontrol.Credentials;
029    import de.deepamehta.core.service.accesscontrol.Operation;
030    import de.deepamehta.core.service.accesscontrol.Permissions;
031    import de.deepamehta.core.service.accesscontrol.SharingMode;
032    import de.deepamehta.core.service.event.PostCreateAssociationListener;
033    import de.deepamehta.core.service.event.PostCreateTopicListener;
034    import de.deepamehta.core.service.event.PostUpdateAssociationListener;
035    import de.deepamehta.core.service.event.PostUpdateTopicListener;
036    import de.deepamehta.core.service.event.PreGetAssociationListener;
037    import de.deepamehta.core.service.event.PreGetTopicListener;
038    import de.deepamehta.core.service.event.ResourceRequestFilterListener;
039    import de.deepamehta.core.service.event.ServiceRequestFilterListener;
040    import de.deepamehta.core.storage.spi.DeepaMehtaTransaction;
041    import de.deepamehta.core.util.JavaUtils;
042    
043    // ### TODO: hide Jersey internals. Move to JAX-RS 2.0.
044    import com.sun.jersey.spi.container.ContainerRequest;
045    
046    import javax.servlet.http.HttpServletRequest;
047    import javax.servlet.http.HttpSession;
048    
049    import javax.ws.rs.GET;
050    import javax.ws.rs.PUT;
051    import javax.ws.rs.POST;
052    import javax.ws.rs.DELETE;
053    import javax.ws.rs.Consumes;
054    import javax.ws.rs.Path;
055    import javax.ws.rs.PathParam;
056    import javax.ws.rs.Produces;
057    import javax.ws.rs.WebApplicationException;
058    import javax.ws.rs.core.Context;
059    import javax.ws.rs.core.Response;
060    import javax.ws.rs.core.Response.Status;
061    
062    import java.util.Collection;
063    import java.util.Enumeration;
064    import java.util.logging.Logger;
065    
066    
067    
068    @Path("/accesscontrol")
069    @Consumes("application/json")
070    @Produces("application/json")
071    public class AccessControlPlugin extends PluginActivator implements AccessControlService, PreGetTopicListener,
072                                                                                             PreGetAssociationListener,
073                                                                                             PostCreateTopicListener,
074                                                                                             PostCreateAssociationListener,
075                                                                                             PostUpdateTopicListener,
076                                                                                             PostUpdateAssociationListener,
077                                                                                             ServiceRequestFilterListener,
078                                                                                             ResourceRequestFilterListener {
079    
080        // ------------------------------------------------------------------------------------------------------- Constants
081    
082        // Security settings
083        private static final boolean READ_REQUIRES_LOGIN  = Boolean.parseBoolean(
084            System.getProperty("dm4.security.read_requires_login", "false"));
085        private static final boolean WRITE_REQUIRES_LOGIN = Boolean.parseBoolean(
086            System.getProperty("dm4.security.write_requires_login", "true"));
087        private static final String SUBNET_FILTER = System.getProperty("dm4.security.subnet_filter", "127.0.0.1/32");
088        // Note: the default values are required in case no config file is in effect. This applies when DM is started
089        // via feature:install from Karaf. The default values must match the values defined in global POM.
090    
091        private static final String AUTHENTICATION_REALM = "DeepaMehta";
092    
093        // Associations
094        private static final String MEMBERSHIP_TYPE = "dm4.accesscontrol.membership";
095    
096        // Property URIs
097        private static String PROP_CREATOR  = "dm4.accesscontrol.creator";
098        private static String PROP_OWNER    = "dm4.accesscontrol.owner";
099        private static String PROP_MODIFIER = "dm4.accesscontrol.modifier";
100    
101        // Events
102        private static DeepaMehtaEvent POST_LOGIN_USER = new DeepaMehtaEvent(PostLoginUserListener.class) {
103            @Override
104            public void deliver(EventListener listener, Object... params) {
105                ((PostLoginUserListener) listener).postLoginUser(
106                    (String) params[0]
107                );
108            }
109        };
110        private static DeepaMehtaEvent POST_LOGOUT_USER = new DeepaMehtaEvent(PostLogoutUserListener.class) {
111            @Override
112            public void deliver(EventListener listener, Object... params) {
113                ((PostLogoutUserListener) listener).postLogoutUser(
114                    (String) params[0]
115                );
116            }
117        };
118    
119        // ---------------------------------------------------------------------------------------------- Instance Variables
120    
121        @Inject
122        private WorkspacesService wsService;
123    
124        @Context
125        private HttpServletRequest request;
126    
127        private Logger logger = Logger.getLogger(getClass().getName());
128    
129        // -------------------------------------------------------------------------------------------------- Public Methods
130    
131    
132    
133        // *******************************************
134        // *** AccessControlService Implementation ***
135        // *******************************************
136    
137    
138    
139        // === User Session ===
140    
141        @POST
142        @Path("/login")
143        @Override
144        public void login() {
145            // Note: the actual login is performed by the request filter. See requestFilter().
146        }
147    
148        @POST
149        @Path("/logout")
150        @Override
151        public void logout() {
152            _logout(request);
153            //
154            // For a "private" DeepaMehta installation: emulate a HTTP logout by forcing the webbrowser to bring up its
155            // login dialog and to forget the former Authorization information. The user is supposed to press "Cancel".
156            // The login dialog can't be used to login again.
157            if (READ_REQUIRES_LOGIN) {
158                throw401Unauthorized();
159            }
160        }
161    
162        // ---
163    
164        @GET
165        @Path("/user")
166        @Produces("text/plain")
167        @Override
168        public String getUsername() {
169            try {
170                HttpSession session = request.getSession(false);    // create=false
171                if (session == null) {
172                    return null;
173                }
174                return username(session);
175            } catch (IllegalStateException e) {
176                // Note: this happens if a request method is called outside request scope.
177                // This is the case while system startup.
178                return null;    // user is unknown
179            }
180        }
181    
182    
183    
184        // === User Accounts ===
185    
186        @POST
187        @Path("/user_account")
188        @Transactional
189        @Override
190        public Topic createUserAccount(Credentials cred) {
191            String username = cred.username;
192            logger.info("Creating user account \"" + username + "\"");
193            //
194            // 1) create user account
195            Topic userAccount = dms.createTopic(new TopicModel("dm4.accesscontrol.user_account", new ChildTopicsModel()
196                .put("dm4.accesscontrol.username", username)
197                .put("dm4.accesscontrol.password", cred.password)));
198            ChildTopics childTopics = userAccount.getChildTopics();
199            Topic usernameTopic = childTopics.getTopic("dm4.accesscontrol.username");
200            Topic passwordTopic = childTopics.getTopic("dm4.accesscontrol.password");
201            //
202            // 2) create private workspace
203            Topic privateWorkspace = wsService.createWorkspace(DEFAULT_PRIVATE_WORKSPACE_NAME, null, SharingMode.PRIVATE);
204            setWorkspaceOwner(privateWorkspace, username);
205            // Note: we don't set a particular creator/modifier here as we don't want suggest that the new user's private
206            // workspace has been created by the new user itself. Instead we set the *current* user as the creator/modifier
207            // (via postCreateTopic() listener). In case of the "admin" user account the creator/modifier remain undefined
208            // as it is actually created by the system itself.
209            //
210            // 3) assign user account and password to private workspace
211            // Note: the current user has no READ access to the private workspace just created.
212            // So we must use the privileged assignToWorkspace calls here (instead of using the Workspaces service).
213            long privateWorkspaceId = privateWorkspace.getId();
214            dms.getAccessControl().assignToWorkspace(userAccount, privateWorkspaceId);
215            dms.getAccessControl().assignToWorkspace(passwordTopic, privateWorkspaceId);
216            //
217            // 4) assign username to "System" workspace
218            Topic systemWorkspace = wsService.getWorkspace(SYSTEM_WORKSPACE_URI);
219            wsService.assignToWorkspace(usernameTopic, systemWorkspace.getId());
220            //
221            return usernameTopic;
222        }
223    
224        @GET
225        @Path("/user/workspace")
226        @Override
227        public Topic getPrivateWorkspace() {
228            String username = getUsername();
229            if (username == null) {
230                throw new IllegalStateException("No user is logged in");
231            }
232            //
233            Topic passwordTopic = getPasswordTopic(getUserAccount(getUsernameTopic(username)));
234            Topic workspace = wsService.getAssignedWorkspace(passwordTopic.getId());
235            if (workspace == null) {
236                throw new RuntimeException("User \"" + username + "\" has no private workspace");
237            }
238            return workspace;
239        }
240    
241        @Override
242        public Topic getUsernameTopic(String username) {
243            return dms.getTopic("dm4.accesscontrol.username", new SimpleValue(username));
244        }
245    
246    
247    
248        // === Workspaces / Memberships ===
249    
250        @GET
251        @Path("/workspace/{workspace_id}/owner")
252        @Produces("text/plain")
253        @Override
254        public String getWorkspaceOwner(@PathParam("workspace_id") long workspaceId) {
255            // ### TODO: delegate to Core's AccessControl.getOwner()?
256            return dms.hasProperty(workspaceId, PROP_OWNER) ? (String) dms.getProperty(workspaceId, PROP_OWNER) : null;
257        }
258    
259        @Override
260        public void setWorkspaceOwner(Topic workspace, String username) {
261            try {
262                workspace.setProperty(PROP_OWNER, username, true);  // addToIndex=true
263            } catch (Exception e) {
264                throw new RuntimeException("Setting the workspace owner of " + info(workspace) + " failed (username=" +
265                    username + ")", e);
266            }
267        }
268    
269        // ---
270    
271        @POST
272        @Path("/user/{username}/workspace/{workspace_id}")
273        @Transactional
274        @Override
275        public void createMembership(@PathParam("username") String username, @PathParam("workspace_id") long workspaceId) {
276            try {
277                dms.createAssociation(new AssociationModel(MEMBERSHIP_TYPE,
278                    new TopicRoleModel(getUsernameTopicOrThrow(username).getId(), "dm4.core.default"),
279                    new TopicRoleModel(workspaceId, "dm4.core.default")
280                ));
281            } catch (Exception e) {
282                throw new RuntimeException("Creating membership for user \"" + username + "\" and workspace " +
283                    workspaceId + " failed", e);
284            }
285        }
286    
287        @Override
288        public boolean isMember(String username, long workspaceId) {
289            return dms.getAccessControl().isMember(username, workspaceId);
290        }
291    
292    
293    
294        // === Permissions ===
295    
296        @GET
297        @Path("/topic/{id}")
298        @Override
299        public Permissions getTopicPermissions(@PathParam("id") long topicId) {
300            return getPermissions(topicId);
301        }
302    
303        @GET
304        @Path("/association/{id}")
305        @Override
306        public Permissions getAssociationPermissions(@PathParam("id") long assocId) {
307            return getPermissions(assocId);
308        }
309    
310    
311    
312        // === Object Info ===
313    
314        @GET
315        @Path("/object/{id}/creator")
316        @Produces("text/plain")
317        @Override
318        public String getCreator(@PathParam("id") long objectId) {
319            return dms.hasProperty(objectId, PROP_CREATOR) ? (String) dms.getProperty(objectId, PROP_CREATOR) : null;
320        }
321    
322        @GET
323        @Path("/object/{id}/modifier")
324        @Produces("text/plain")
325        @Override
326        public String getModifier(@PathParam("id") long objectId) {
327            return dms.hasProperty(objectId, PROP_MODIFIER) ? (String) dms.getProperty(objectId, PROP_MODIFIER) : null;
328        }
329    
330    
331    
332        // === Retrieval ===
333    
334        @GET
335        @Path("/creator/{username}/topics")
336        @Override
337        public Collection<Topic> getTopicsByCreator(@PathParam("username") String username) {
338            return dms.getTopicsByProperty(PROP_CREATOR, username);
339        }
340    
341        @GET
342        @Path("/owner/{username}/topics")
343        @Override
344        public Collection<Topic> getTopicsByOwner(@PathParam("username") String username) {
345            return dms.getTopicsByProperty(PROP_OWNER, username);
346        }
347    
348        @GET
349        @Path("/creator/{username}/assocs")
350        @Override
351        public Collection<Association> getAssociationsByCreator(@PathParam("username") String username) {
352            return dms.getAssociationsByProperty(PROP_CREATOR, username);
353        }
354    
355        @GET
356        @Path("/owner/{username}/assocs")
357        @Override
358        public Collection<Association> getAssociationsByOwner(@PathParam("username") String username) {
359            return dms.getAssociationsByProperty(PROP_OWNER, username);
360        }
361    
362    
363    
364        // ****************************
365        // *** Hook Implementations ***
366        // ****************************
367    
368    
369    
370        @Override
371        public void init() {
372            logger.info("Security settings:" +
373                "\ndm4.security.read_requires_login=" + READ_REQUIRES_LOGIN +
374                "\ndm4.security.write_requires_login=" + WRITE_REQUIRES_LOGIN +
375                "\ndm4.security.subnet_filter=\"" + SUBNET_FILTER + "\"");
376        }
377    
378    
379    
380        // ********************************
381        // *** Listener Implementations ***
382        // ********************************
383    
384    
385    
386        @Override
387        public void preGetTopic(long topicId) {
388            checkReadPermission(topicId);
389        }
390    
391        @Override
392        public void preGetAssociation(long assocId) {
393            checkReadPermission(assocId);
394            //
395            long[] playerIds = dms.getPlayerIds(assocId);
396            checkReadPermission(playerIds[0]);
397            checkReadPermission(playerIds[1]);
398        }
399    
400        // ---
401    
402        @Override
403        public void postCreateTopic(Topic topic) {
404            String typeUri = topic.getTypeUri();
405            if (typeUri.equals("dm4.workspaces.workspace")) {
406                setWorkspaceOwner(topic);
407            } else if (typeUri.equals("dm4.webclient.search")) {
408                assignSearchTopic(topic);
409            }
410            //
411            setCreatorAndModifier(topic);
412        }
413    
414        @Override
415        public void postCreateAssociation(Association assoc) {
416            setCreatorAndModifier(assoc);
417        }
418    
419        // ---
420    
421        // ### TODO: revise/drop this method. Meanwhile a user account is created via dialog.
422        @Override
423        public void postUpdateTopic(Topic topic, TopicModel newModel, TopicModel oldModel) {
424            if (topic.getTypeUri().equals("dm4.accesscontrol.user_account")) {
425                Topic usernameTopic = topic.getChildTopics().getTopic("dm4.accesscontrol.username");
426                Topic passwordTopic = topic.getChildTopics().getTopic("dm4.accesscontrol.password");
427                String newUsername = usernameTopic.getSimpleValue().toString();
428                TopicModel oldUsernameTopic = oldModel.getChildTopicsModel().getTopic("dm4.accesscontrol.username",
429                    null);
430                String oldUsername = oldUsernameTopic != null ? oldUsernameTopic.getSimpleValue().toString() : "";
431                if (!newUsername.equals(oldUsername)) {
432                    //
433                    if (!oldUsername.equals("")) {
434                        throw new RuntimeException("Changing a Username is not supported (tried \"" + oldUsername +
435                            "\" -> \"" + newUsername + "\")");
436                    }
437                    //
438                    logger.info("### Username has changed from \"" + oldUsername + "\" -> \"" + newUsername +
439                        "\". Setting \"" + newUsername + "\" as the new owner of 3 topics:\n" +
440                        "    - User Account topic (ID " + topic.getId() + ")\n" + 
441                        "    - Username topic (ID " + usernameTopic.getId() + ")\n" + 
442                        "    - Password topic (ID " + passwordTopic.getId() + ")");
443                    // ### setOwner(topic, newUsername);
444                    // ### setOwner(usernameTopic, newUsername);
445                    // ### setOwner(passwordTopic, newUsername);
446                }
447            }
448            //
449            setModifier(topic);
450        }
451    
452        @Override
453        public void postUpdateAssociation(Association assoc, AssociationModel oldModel) {
454            if (isMembership(assoc.getModel())) {
455                if (isMembership(oldModel)) {
456                    // ### TODO?
457                } else {
458                    wsService.assignToWorkspace(assoc, assoc.getTopicByType("dm4.workspaces.workspace").getId());
459                }
460            } else if (isMembership(oldModel)) {
461                // ### TODO?
462            }
463            //
464            setModifier(assoc);
465        }
466    
467        // ---
468    
469        @Override
470        public void serviceRequestFilter(ContainerRequest containerRequest) {
471            // Note: we pass the injected HttpServletRequest
472            requestFilter(request);
473        }
474    
475        @Override
476        public void resourceRequestFilter(HttpServletRequest servletRequest) {
477            // Note: for the resource filter no HttpServletRequest is injected
478            requestFilter(servletRequest);
479        }
480    
481    
482    
483        // ------------------------------------------------------------------------------------------------- Private Methods
484    
485        private Topic getUserAccount(Topic usernameTopic) {
486            return usernameTopic.getRelatedTopic("dm4.core.composition", "dm4.core.child", "dm4.core.parent",
487                "dm4.accesscontrol.user_account");
488        }
489    
490        private Topic getPasswordTopic(Topic userAccount) {
491            return userAccount.getChildTopics().getTopic("dm4.accesscontrol.password");
492        }
493    
494        private Topic getUsernameTopicOrThrow(String username) {
495            Topic usernameTopic = getUsernameTopic(username);
496            if (usernameTopic == null) {
497                throw new RuntimeException("User \"" + username + "\" does not exist");
498            }
499            return usernameTopic;
500        }
501    
502        private boolean isMembership(AssociationModel assoc) {
503            return assoc.getTypeUri().equals(MEMBERSHIP_TYPE);
504        }
505    
506        private void assignSearchTopic(Topic searchTopic) {
507            try {
508                Topic workspace;
509                if (getUsername() != null) {
510                    workspace = getPrivateWorkspace();
511                } else {
512                    workspace = wsService.getWorkspace(WorkspacesService.DEEPAMEHTA_WORKSPACE_URI);
513                }
514                wsService.assignToWorkspace(searchTopic, workspace.getId());
515            } catch (Exception e) {
516                throw new RuntimeException("Assigning search topic to workspace failed", e);
517            }
518        }
519    
520    
521    
522        // === Request Filter ===
523    
524        private void requestFilter(HttpServletRequest request) {
525            logger.fine("##### " + request.getMethod() + " " + request.getRequestURL() +
526                "\n      ##### \"Authorization\"=\"" + request.getHeader("Authorization") + "\"" +
527                "\n      ##### " + info(request.getSession(false)));    // create=false
528            //
529            checkRequestOrigin(request);    // throws WebApplicationException
530            checkAuthorization(request);    // throws WebApplicationException
531        }
532    
533        // ---
534    
535        private void checkRequestOrigin(HttpServletRequest request) {
536            String remoteAddr = request.getRemoteAddr();
537            boolean allowed = JavaUtils.isInRange(remoteAddr, SUBNET_FILTER);
538            //
539            logger.fine("Remote address=\"" + remoteAddr + "\", dm4.security.subnet_filter=\"" + SUBNET_FILTER +
540                "\" => " + (allowed ? "ALLOWED" : "FORBIDDEN"));
541            //
542            if (!allowed) {
543                throw403Forbidden();    // throws WebApplicationException
544            }
545        }
546    
547        private void checkAuthorization(HttpServletRequest request) {
548            boolean authorized;
549            if (request.getSession(false) != null) {    // create=false
550                authorized = true;
551            } else {
552                String authHeader = request.getHeader("Authorization");
553                if (authHeader != null) {
554                    // Note: if login fails we are NOT authorized, even if no login is required
555                    authorized = tryLogin(new Credentials(authHeader), request);
556                } else {
557                    authorized = !isLoginRequired(request);
558                }
559            }
560            //
561            if (!authorized) {
562                throw401Unauthorized(); // throws WebApplicationException
563            }
564        }
565    
566        // ---
567    
568        private boolean isLoginRequired(HttpServletRequest request) {
569            return request.getMethod().equals("GET") ? READ_REQUIRES_LOGIN : WRITE_REQUIRES_LOGIN;
570        }
571    
572        /**
573         * Checks weather the credentials are valid and if so logs the user in.
574         *
575         * @return  true if the credentials are valid.
576         */
577        private boolean tryLogin(Credentials cred, HttpServletRequest request) {
578            String username = cred.username;
579            if (checkCredentials(cred)) {
580                logger.info("##### Logging in as \"" + username + "\" => SUCCESSFUL!");
581                _login(username, request);
582                return true;
583            } else {
584                logger.info("##### Logging in as \"" + username + "\" => FAILED!");
585                return false;
586            }
587        }
588    
589        private boolean checkCredentials(Credentials cred) {
590            return dms.getAccessControl().checkCredentials(cred);
591        }
592    
593        // ---
594    
595        private void _login(String username, HttpServletRequest request) {
596            HttpSession session = request.getSession();
597            session.setAttribute("username", username);
598            logger.info("##### Creating new " + info(session));
599            //
600            dms.fireEvent(POST_LOGIN_USER, username);
601        }
602    
603        private void _logout(HttpServletRequest request) {
604            HttpSession session = request.getSession(false);    // create=false
605            String username = username(session);                // save username before invalidating
606            logger.info("##### Logging out from " + info(session));
607            //
608            session.invalidate();
609            //
610            dms.fireEvent(POST_LOGOUT_USER, username);
611        }
612    
613        // ---
614    
615        private String username(HttpSession session) {
616            String username = (String) session.getAttribute("username");
617            if (username == null) {
618                throw new RuntimeException("Session data inconsistency: \"username\" attribute is missing");
619            }
620            return username;
621        }
622    
623        // ---
624    
625        private void throw401Unauthorized() {
626            // Note: a non-private DM installation (read_requires_login=false) utilizes DM's login dialog and must suppress
627            // the browser's login dialog. To suppress the browser's login dialog a contrived authentication scheme "xBasic"
628            // is used (see http://loudvchar.blogspot.ca/2010/11/avoiding-browser-popup-for-401.html)
629            String authScheme = READ_REQUIRES_LOGIN ? "Basic" : "xBasic";
630            throw new WebApplicationException(Response.status(Status.UNAUTHORIZED)
631                .header("WWW-Authenticate", authScheme + " realm=" + AUTHENTICATION_REALM)
632                .header("Content-Type", "text/html")    // for text/plain (default) Safari provides no Web Console
633                .entity("You're not authorized. Sorry.")
634                .build());
635        }
636    
637        private void throw403Forbidden() {
638            throw new WebApplicationException(Response.status(Status.FORBIDDEN)
639                .header("Content-Type", "text/html")    // for text/plain (default) Safari provides no Web Console
640                .entity("Access is forbidden. Sorry.")
641                .build());
642        }
643    
644    
645    
646        // === Setup Access Control ===
647    
648        /**
649         * Sets the logged in user as the creator/modifier of the given object.
650         * <p>
651         * If no user is logged in, nothing is performed.
652         */
653        private void setCreatorAndModifier(DeepaMehtaObject object) {
654            try {
655                String username = getUsername();
656                // Note: when no user is logged in we do NOT fallback to the default user for the access control setup.
657                // This would not help in gaining data consistency because the topics/associations created so far
658                // (BEFORE the Access Control plugin is activated) would still have no access control setup.
659                // Note: for types the situation is different. The type-introduction mechanism (see introduceTopicType()
660                // handler above) ensures EVERY type is catched (regardless of plugin activation order). For instances on
661                // the other hand we don't have such a mechanism (and don't want one either).
662                if (username == null) {
663                    logger.fine("Setting the creator/modifier of " + info(object) + " ABORTED -- no user is logged in");
664                    return;
665                }
666                //
667                setCreatorAndModifier(object, username);
668            } catch (Exception e) {
669                throw new RuntimeException("Setting the creator/modifier of " + info(object) + " failed", e);
670            }
671        }
672    
673        /**
674         * @param   username    must not be null.
675         */
676        private void setCreatorAndModifier(DeepaMehtaObject object, String username) {
677            setCreator(object, username);
678            setModifier(object, username);
679        }
680    
681        // ---
682    
683        /**
684         * Sets the creator of a topic or an association.
685         */
686        private void setCreator(DeepaMehtaObject object, String username) {
687            try {
688                object.setProperty(PROP_CREATOR, username, true);   // addToIndex=true
689            } catch (Exception e) {
690                throw new RuntimeException("Setting the creator of " + info(object) + " failed (username=" + username + ")",
691                    e);
692            }
693        }
694    
695        // ---
696    
697        private void setModifier(DeepaMehtaObject object) {
698            String username = getUsername();
699            // Note: when a plugin topic is updated there is no user logged in yet.
700            if (username == null) {
701                return;
702            }
703            //
704            setModifier(object, username);
705        }
706    
707        private void setModifier(DeepaMehtaObject object, String username) {
708            object.setProperty(PROP_MODIFIER, username, false);     // addToIndex=false
709        }
710    
711        // ---
712    
713        private void setWorkspaceOwner(Topic workspace) {
714            String username = getUsername();
715            // Note: username is null if the Access Control plugin is activated already
716            // when a 3rd-party plugin creates a workspace at install-time.
717            if (username == null) {
718                return;
719            }
720            //
721            setWorkspaceOwner(workspace, username);
722        }
723    
724    
725    
726        // === Calculate Permissions ===
727    
728        /**
729         * @param   objectId    a topic ID, or an association ID
730         */
731        private void checkReadPermission(long objectId) {
732            if (!inRequestScope()) {
733                logger.fine("### Object " + objectId + " is accessed by \"System\" -- READ permission is granted");
734                return;
735            }
736            //
737            String username = getUsername();
738            if (!hasPermission(username, Operation.READ, objectId)) {
739                throw new AccessControlException(userInfo(username) + " has no READ permission for object " + objectId);
740            }
741        }
742    
743        /**
744         * @param   objectId    a topic ID, or an association ID.
745         */
746        private Permissions getPermissions(long objectId) {
747            return new Permissions().add(Operation.WRITE, hasPermission(getUsername(), Operation.WRITE, objectId));
748        }
749    
750        /**
751         * Checks if a user is permitted to perform an operation on an object (topic or association).
752         *
753         * @param   username    the logged in user, or <code>null</code> if no user is logged in.
754         * @param   objectId    a topic ID, or an association ID.
755         *
756         * @return  <code>true</code> if permission is granted, <code>false</code> otherwise.
757         */
758        private boolean hasPermission(String username, Operation operation, long objectId) {
759            return dms.getAccessControl().hasPermission(username, operation, objectId);
760        }
761    
762        private boolean inRequestScope() {
763            try {
764                request.getMethod();
765                return true;
766            } catch (IllegalStateException e) {
767                // Note: this happens if a request method is called outside request scope.
768                // This is the case while system startup.
769                return false;
770            }
771        }
772    
773    
774    
775        // === Logging ===
776    
777        private String info(DeepaMehtaObject object) {
778            if (object instanceof TopicType) {
779                return "topic type \"" + object.getUri() + "\" (id=" + object.getId() + ")";
780            } else if (object instanceof AssociationType) {
781                return "association type \"" + object.getUri() + "\" (id=" + object.getId() + ")";
782            } else if (object instanceof Topic) {
783                return "topic " + object.getId() + " (typeUri=\"" + object.getTypeUri() + "\", uri=\"" + object.getUri() +
784                    "\")";
785            } else if (object instanceof Association) {
786                return "association " + object.getId() + " (typeUri=\"" + object.getTypeUri() + "\")";
787            } else {
788                throw new RuntimeException("Unexpected object: " + object);
789            }
790        }
791    
792        private String userInfo(String username) {
793            return "user " + (username != null ? "\"" + username + "\"" : "<anonymous>");
794        }
795    
796        private String info(HttpSession session) {
797            return "session" + (session != null ? " " + session.getId() +
798                " (username=" + username(session) + ")" : ": null");
799        }
800    
801        private String info(HttpServletRequest request) {
802            StringBuilder info = new StringBuilder();
803            info.append("    " + request.getMethod() + " " + request.getRequestURI() + "\n");
804            Enumeration<String> e1 = request.getHeaderNames();
805            while (e1.hasMoreElements()) {
806                String name = e1.nextElement();
807                info.append("\n    " + name + ":");
808                Enumeration<String> e2 = request.getHeaders(name);
809                while (e2.hasMoreElements()) {
810                    String header = e2.nextElement();
811                    info.append(" " + header);
812                }
813            }
814            return info.toString();
815        }
816    }