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