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: if not invoked through network no request (and thus no session) is available.
177                // This happens e.g. while starting up.
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        @Override
225        public Topic getPrivateWorkspace() {
226            String username = getUsername();
227            if (username == null) {
228                throw new IllegalStateException("No user is logged in");
229            }
230            //
231            Topic passwordTopic = getPasswordTopic(getUserAccount(getUsernameTopic(username)));
232            Topic workspace = wsService.getAssignedWorkspace(passwordTopic.getId());
233            if (workspace == null) {
234                throw new RuntimeException("User \"" + username + "\" has no private workspace");
235            }
236            return workspace;
237        }
238    
239        @Override
240        public Topic getUsernameTopic(String username) {
241            return dms.getTopic("dm4.accesscontrol.username", new SimpleValue(username));
242        }
243    
244    
245    
246        // === Workspaces / Memberships ===
247    
248        @GET
249        @Path("/workspace/{workspace_id}/owner")
250        @Produces("text/plain")
251        @Override
252        public String getWorkspaceOwner(@PathParam("workspace_id") long workspaceId) {
253            // ### TODO: delegate to Core's AccessControl.getOwner()?
254            return dms.hasProperty(workspaceId, PROP_OWNER) ? (String) dms.getProperty(workspaceId, PROP_OWNER) : null;
255        }
256    
257        @Override
258        public void setWorkspaceOwner(Topic workspace, String username) {
259            try {
260                workspace.setProperty(PROP_OWNER, username, true);  // addToIndex=true
261            } catch (Exception e) {
262                throw new RuntimeException("Setting the workspace owner of " + info(workspace) + " failed (username=" +
263                    username + ")", e);
264            }
265        }
266    
267        // ---
268    
269        @POST
270        @Path("/user/{username}/workspace/{workspace_id}")
271        @Transactional
272        @Override
273        public void createMembership(@PathParam("username") String username, @PathParam("workspace_id") long workspaceId) {
274            try {
275                dms.createAssociation(new AssociationModel(MEMBERSHIP_TYPE,
276                    new TopicRoleModel(getUsernameTopicOrThrow(username).getId(), "dm4.core.default"),
277                    new TopicRoleModel(workspaceId, "dm4.core.default")
278                ));
279            } catch (Exception e) {
280                throw new RuntimeException("Creating membership for user \"" + username + "\" and workspace " +
281                    workspaceId + " failed", e);
282            }
283        }
284    
285        @Override
286        public boolean isMember(String username, long workspaceId) {
287            return dms.getAccessControl().isMember(username, workspaceId);
288        }
289    
290    
291    
292        // === Permissions ===
293    
294        @GET
295        @Path("/topic/{id}")
296        @Override
297        public Permissions getTopicPermissions(@PathParam("id") long topicId) {
298            return getPermissions(topicId);
299        }
300    
301        @GET
302        @Path("/association/{id}")
303        @Override
304        public Permissions getAssociationPermissions(@PathParam("id") long assocId) {
305            return getPermissions(assocId);
306        }
307    
308    
309    
310        // === Object Info ===
311    
312        @GET
313        @Path("/object/{id}/creator")
314        @Produces("text/plain")
315        @Override
316        public String getCreator(@PathParam("id") long objectId) {
317            return dms.hasProperty(objectId, PROP_CREATOR) ? (String) dms.getProperty(objectId, PROP_CREATOR) : null;
318        }
319    
320        @GET
321        @Path("/object/{id}/modifier")
322        @Produces("text/plain")
323        @Override
324        public String getModifier(@PathParam("id") long objectId) {
325            return dms.hasProperty(objectId, PROP_MODIFIER) ? (String) dms.getProperty(objectId, PROP_MODIFIER) : null;
326        }
327    
328    
329    
330        // === Retrieval ===
331    
332        @GET
333        @Path("/creator/{username}/topics")
334        @Override
335        public Collection<Topic> getTopicsByCreator(@PathParam("username") String username) {
336            return dms.getTopicsByProperty(PROP_CREATOR, username);
337        }
338    
339        @GET
340        @Path("/owner/{username}/topics")
341        @Override
342        public Collection<Topic> getTopicsByOwner(@PathParam("username") String username) {
343            return dms.getTopicsByProperty(PROP_OWNER, username);
344        }
345    
346        @GET
347        @Path("/creator/{username}/assocs")
348        @Override
349        public Collection<Association> getAssociationsByCreator(@PathParam("username") String username) {
350            return dms.getAssociationsByProperty(PROP_CREATOR, username);
351        }
352    
353        @GET
354        @Path("/owner/{username}/assocs")
355        @Override
356        public Collection<Association> getAssociationsByOwner(@PathParam("username") String username) {
357            return dms.getAssociationsByProperty(PROP_OWNER, username);
358        }
359    
360    
361    
362        // ****************************
363        // *** Hook Implementations ***
364        // ****************************
365    
366    
367    
368        @Override
369        public void init() {
370            logger.info("Security settings:" +
371                "\ndm4.security.read_requires_login=" + READ_REQUIRES_LOGIN +
372                "\ndm4.security.write_requires_login=" + WRITE_REQUIRES_LOGIN +
373                "\ndm4.security.subnet_filter=\"" + SUBNET_FILTER + "\"");
374        }
375    
376    
377    
378        // ********************************
379        // *** Listener Implementations ***
380        // ********************************
381    
382    
383    
384        @Override
385        public void preGetTopic(long topicId) {
386            checkReadPermission(topicId);
387        }
388    
389        @Override
390        public void preGetAssociation(long assocId) {
391            checkReadPermission(assocId);
392            //
393            long[] playerIds = dms.getPlayerIds(assocId);
394            checkReadPermission(playerIds[0]);
395            checkReadPermission(playerIds[1]);
396        }
397    
398        // ---
399    
400        @Override
401        public void postCreateTopic(Topic topic) {
402            String typeUri = topic.getTypeUri();
403            if (typeUri.equals("dm4.workspaces.workspace")) {
404                setWorkspaceOwner(topic);
405            } else if (typeUri.equals("dm4.webclient.search")) {
406                assignSearchTopic(topic);
407            }
408            //
409            setCreatorAndModifier(topic);
410        }
411    
412        @Override
413        public void postCreateAssociation(Association assoc) {
414            setCreatorAndModifier(assoc);
415        }
416    
417        // ---
418    
419        // ### TODO: revise/drop this method. Meanwhile a user account is created via dialog.
420        @Override
421        public void postUpdateTopic(Topic topic, TopicModel newModel, TopicModel oldModel) {
422            if (topic.getTypeUri().equals("dm4.accesscontrol.user_account")) {
423                Topic usernameTopic = topic.getChildTopics().getTopic("dm4.accesscontrol.username");
424                Topic passwordTopic = topic.getChildTopics().getTopic("dm4.accesscontrol.password");
425                String newUsername = usernameTopic.getSimpleValue().toString();
426                TopicModel oldUsernameTopic = oldModel.getChildTopicsModel().getTopic("dm4.accesscontrol.username",
427                    null);
428                String oldUsername = oldUsernameTopic != null ? oldUsernameTopic.getSimpleValue().toString() : "";
429                if (!newUsername.equals(oldUsername)) {
430                    //
431                    if (!oldUsername.equals("")) {
432                        throw new RuntimeException("Changing a Username is not supported (tried \"" + oldUsername +
433                            "\" -> \"" + newUsername + "\")");
434                    }
435                    //
436                    logger.info("### Username has changed from \"" + oldUsername + "\" -> \"" + newUsername +
437                        "\". Setting \"" + newUsername + "\" as the new owner of 3 topics:\n" +
438                        "    - User Account topic (ID " + topic.getId() + ")\n" + 
439                        "    - Username topic (ID " + usernameTopic.getId() + ")\n" + 
440                        "    - Password topic (ID " + passwordTopic.getId() + ")");
441                    // ### setOwner(topic, newUsername);
442                    // ### setOwner(usernameTopic, newUsername);
443                    // ### setOwner(passwordTopic, newUsername);
444                }
445            }
446            //
447            setModifier(topic);
448        }
449    
450        @Override
451        public void postUpdateAssociation(Association assoc, AssociationModel oldModel) {
452            if (isMembership(assoc.getModel())) {
453                if (isMembership(oldModel)) {
454                    // ### TODO?
455                } else {
456                    wsService.assignToWorkspace(assoc, assoc.getTopicByType("dm4.workspaces.workspace").getId());
457                }
458            } else if (isMembership(oldModel)) {
459                // ### TODO?
460            }
461            //
462            setModifier(assoc);
463        }
464    
465        // ---
466    
467        @Override
468        public void serviceRequestFilter(ContainerRequest containerRequest) {
469            // Note: we pass the injected HttpServletRequest
470            requestFilter(request);
471        }
472    
473        @Override
474        public void resourceRequestFilter(HttpServletRequest servletRequest) {
475            // Note: for the resource filter no HttpServletRequest is injected
476            requestFilter(servletRequest);
477        }
478    
479    
480    
481        // ------------------------------------------------------------------------------------------------- Private Methods
482    
483        private Topic getUserAccount(Topic usernameTopic) {
484            return usernameTopic.getRelatedTopic("dm4.core.composition", "dm4.core.child", "dm4.core.parent",
485                "dm4.accesscontrol.user_account");
486        }
487    
488        private Topic getPasswordTopic(Topic userAccount) {
489            return userAccount.getChildTopics().getTopic("dm4.accesscontrol.password");
490        }
491    
492        private Topic getUsernameTopicOrThrow(String username) {
493            Topic usernameTopic = getUsernameTopic(username);
494            if (usernameTopic == null) {
495                throw new RuntimeException("User \"" + username + "\" does not exist");
496            }
497            return usernameTopic;
498        }
499    
500        private boolean isMembership(AssociationModel assoc) {
501            return assoc.getTypeUri().equals(MEMBERSHIP_TYPE);
502        }
503    
504        private void assignSearchTopic(Topic searchTopic) {
505            try {
506                Topic workspace;
507                if (getUsername() != null) {
508                    workspace = getPrivateWorkspace();
509                } else {
510                    workspace = wsService.getWorkspace(WorkspacesService.DEEPAMEHTA_WORKSPACE_URI);
511                }
512                wsService.assignToWorkspace(searchTopic, workspace.getId());
513            } catch (Exception e) {
514                throw new RuntimeException("Assigning search topic to workspace failed", e);
515            }
516        }
517    
518    
519    
520        // === Request Filter ===
521    
522        private void requestFilter(HttpServletRequest request) {
523            logger.fine("##### " + request.getMethod() + " " + request.getRequestURL() +
524                "\n      ##### \"Authorization\"=\"" + request.getHeader("Authorization") + "\"" +
525                "\n      ##### " + info(request.getSession(false)));    // create=false
526            //
527            checkRequestOrigin(request);    // throws WebApplicationException
528            checkAuthorization(request);    // throws WebApplicationException
529        }
530    
531        // ---
532    
533        private void checkRequestOrigin(HttpServletRequest request) {
534            String remoteAddr = request.getRemoteAddr();
535            boolean allowed = JavaUtils.isInRange(remoteAddr, SUBNET_FILTER);
536            //
537            logger.fine("Remote address=\"" + remoteAddr + "\", dm4.security.subnet_filter=\"" + SUBNET_FILTER +
538                "\" => " + (allowed ? "ALLOWED" : "FORBIDDEN"));
539            //
540            if (!allowed) {
541                throw403Forbidden();    // throws WebApplicationException
542            }
543        }
544    
545        private void checkAuthorization(HttpServletRequest request) {
546            boolean authorized;
547            if (request.getSession(false) != null) {    // create=false
548                authorized = true;
549            } else {
550                String authHeader = request.getHeader("Authorization");
551                if (authHeader != null) {
552                    // Note: if login fails we are NOT authorized, even if no login is required
553                    authorized = tryLogin(new Credentials(authHeader), request);
554                } else {
555                    authorized = !isLoginRequired(request);
556                }
557            }
558            //
559            if (!authorized) {
560                throw401Unauthorized(); // throws WebApplicationException
561            }
562        }
563    
564        // ---
565    
566        private boolean isLoginRequired(HttpServletRequest request) {
567            return request.getMethod().equals("GET") ? READ_REQUIRES_LOGIN : WRITE_REQUIRES_LOGIN;
568        }
569    
570        /**
571         * Checks weather the credentials are valid and if so logs the user in.
572         *
573         * @return  true if the credentials are valid.
574         */
575        private boolean tryLogin(Credentials cred, HttpServletRequest request) {
576            String username = cred.username;
577            if (checkCredentials(cred)) {
578                logger.info("##### Logging in as \"" + username + "\" => SUCCESSFUL!");
579                _login(username, request);
580                return true;
581            } else {
582                logger.info("##### Logging in as \"" + username + "\" => FAILED!");
583                return false;
584            }
585        }
586    
587        private boolean checkCredentials(Credentials cred) {
588            return dms.getAccessControl().checkCredentials(cred);
589        }
590    
591        // ---
592    
593        private void _login(String username, HttpServletRequest request) {
594            HttpSession session = request.getSession();
595            session.setAttribute("username", username);
596            logger.info("##### Creating new " + info(session));
597            //
598            dms.fireEvent(POST_LOGIN_USER, username);
599        }
600    
601        private void _logout(HttpServletRequest request) {
602            HttpSession session = request.getSession(false);    // create=false
603            String username = username(session);                // save username before invalidating
604            logger.info("##### Logging out from " + info(session));
605            //
606            session.invalidate();
607            //
608            dms.fireEvent(POST_LOGOUT_USER, username);
609        }
610    
611        // ---
612    
613        private String username(HttpSession session) {
614            String username = (String) session.getAttribute("username");
615            if (username == null) {
616                throw new RuntimeException("Session data inconsistency: \"username\" attribute is missing");
617            }
618            return username;
619        }
620    
621        // ---
622    
623        private void throw401Unauthorized() {
624            // Note: a non-private DM installation (read_requires_login=false) utilizes DM's login dialog and must suppress
625            // the browser's login dialog. To suppress the browser's login dialog a contrived authentication scheme "xBasic"
626            // is used (see http://loudvchar.blogspot.ca/2010/11/avoiding-browser-popup-for-401.html)
627            String authScheme = READ_REQUIRES_LOGIN ? "Basic" : "xBasic";
628            throw new WebApplicationException(Response.status(Status.UNAUTHORIZED)
629                .header("WWW-Authenticate", authScheme + " realm=" + AUTHENTICATION_REALM)
630                .header("Content-Type", "text/html")    // for text/plain (default) Safari provides no Web Console
631                .entity("You're not authorized. Sorry.")
632                .build());
633        }
634    
635        private void throw403Forbidden() {
636            throw new WebApplicationException(Response.status(Status.FORBIDDEN)
637                .header("Content-Type", "text/html")    // for text/plain (default) Safari provides no Web Console
638                .entity("Access is forbidden. Sorry.")
639                .build());
640        }
641    
642    
643    
644        // === Setup Access Control ===
645    
646        /**
647         * Sets the logged in user as the creator/modifier of the given object.
648         * <p>
649         * If no user is logged in, nothing is performed.
650         */
651        private void setCreatorAndModifier(DeepaMehtaObject object) {
652            try {
653                String username = getUsername();
654                // Note: when no user is logged in we do NOT fallback to the default user for the access control setup.
655                // This would not help in gaining data consistency because the topics/associations created so far
656                // (BEFORE the Access Control plugin is activated) would still have no access control setup.
657                // Note: for types the situation is different. The type-introduction mechanism (see introduceTopicType()
658                // handler above) ensures EVERY type is catched (regardless of plugin activation order). For instances on
659                // the other hand we don't have such a mechanism (and don't want one either).
660                if (username == null) {
661                    logger.fine("Setting the creator/modifier of " + info(object) + " ABORTED -- no user is logged in");
662                    return;
663                }
664                //
665                setCreatorAndModifier(object, username);
666            } catch (Exception e) {
667                throw new RuntimeException("Setting the creator/modifier of " + info(object) + " failed", e);
668            }
669        }
670    
671        /**
672         * @param   username    must not be null.
673         */
674        private void setCreatorAndModifier(DeepaMehtaObject object, String username) {
675            setCreator(object, username);
676            setModifier(object, username);
677        }
678    
679        // ---
680    
681        /**
682         * Sets the creator of a topic or an association.
683         */
684        private void setCreator(DeepaMehtaObject object, String username) {
685            try {
686                object.setProperty(PROP_CREATOR, username, true);   // addToIndex=true
687            } catch (Exception e) {
688                throw new RuntimeException("Setting the creator of " + info(object) + " failed (username=" + username + ")",
689                    e);
690            }
691        }
692    
693        // ---
694    
695        private void setModifier(DeepaMehtaObject object) {
696            String username = getUsername();
697            // Note: when a plugin topic is updated there is no user logged in yet.
698            if (username == null) {
699                return;
700            }
701            //
702            setModifier(object, username);
703        }
704    
705        private void setModifier(DeepaMehtaObject object, String username) {
706            object.setProperty(PROP_MODIFIER, username, false);     // addToIndex=false
707        }
708    
709        // ---
710    
711        private void setWorkspaceOwner(Topic workspace) {
712            String username = getUsername();
713            // Note: username is null if the Access Control plugin is activated already
714            // when a 3rd-party plugin creates a workspace at install-time.
715            if (username == null) {
716                return;
717            }
718            //
719            setWorkspaceOwner(workspace, username);
720        }
721    
722    
723    
724        // === Calculate Permissions ===
725    
726        /**
727         * @param   objectId    a topic ID, or an association ID
728         */
729        private void checkReadPermission(long objectId) {
730            String username = getUsername();
731            if (!hasPermission(username, Operation.READ, objectId)) {
732                throw new AccessControlException(userInfo(username) + " has no READ permission for object " + objectId);
733            }
734        }
735    
736        /**
737         * @param   objectId    a topic ID, or an association ID.
738         */
739        private Permissions getPermissions(long objectId) {
740            return new Permissions().add(Operation.WRITE, hasPermission(getUsername(), Operation.WRITE, objectId));
741        }
742    
743        /**
744         * Checks if a user is permitted to perform an operation on an object (topic or association).
745         *
746         * @param   username    the logged in user, or <code>null</code> if no user is logged in.
747         * @param   objectId    a topic ID, or an association ID.
748         *
749         * @return  <code>true</code> if permission is granted, <code>false</code> otherwise.
750         */
751        private boolean hasPermission(String username, Operation operation, long objectId) {
752            return dms.getAccessControl().hasPermission(username, operation, objectId);
753        }
754    
755    
756    
757        // === Logging ===
758    
759        private String info(DeepaMehtaObject object) {
760            if (object instanceof TopicType) {
761                return "topic type \"" + object.getUri() + "\" (id=" + object.getId() + ")";
762            } else if (object instanceof AssociationType) {
763                return "association type \"" + object.getUri() + "\" (id=" + object.getId() + ")";
764            } else if (object instanceof Topic) {
765                return "topic " + object.getId() + " (typeUri=\"" + object.getTypeUri() + "\", uri=\"" + object.getUri() +
766                    "\")";
767            } else if (object instanceof Association) {
768                return "association " + object.getId() + " (typeUri=\"" + object.getTypeUri() + "\")";
769            } else {
770                throw new RuntimeException("Unexpected object: " + object);
771            }
772        }
773    
774        private String userInfo(String username) {
775            return "user " + (username != null ? "\"" + username + "\"" : "<anonymous>");
776        }
777    
778        private String info(HttpSession session) {
779            return "session" + (session != null ? " " + session.getId() +
780                " (username=" + username(session) + ")" : ": null");
781        }
782    
783        private String info(HttpServletRequest request) {
784            StringBuilder info = new StringBuilder();
785            info.append("    " + request.getMethod() + " " + request.getRequestURI() + "\n");
786            Enumeration<String> e1 = request.getHeaderNames();
787            while (e1.hasMoreElements()) {
788                String name = e1.nextElement();
789                info.append("\n    " + name + ":");
790                Enumeration<String> e2 = request.getHeaders(name);
791                while (e2.hasMoreElements()) {
792                    String header = e2.nextElement();
793                    info.append(" " + header);
794                }
795            }
796            return info.toString();
797        }
798    }