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.model.AccessControlList;
006    import de.deepamehta.plugins.accesscontrol.model.ACLEntry;
007    import de.deepamehta.plugins.accesscontrol.model.Credentials;
008    import de.deepamehta.plugins.accesscontrol.model.Operation;
009    import de.deepamehta.plugins.accesscontrol.model.Permissions;
010    import de.deepamehta.plugins.accesscontrol.model.UserRole;
011    import de.deepamehta.plugins.accesscontrol.service.AccessControlService;
012    import de.deepamehta.plugins.workspaces.service.WorkspacesService;
013    
014    import de.deepamehta.core.Association;
015    import de.deepamehta.core.AssociationType;
016    import de.deepamehta.core.DeepaMehtaObject;
017    import de.deepamehta.core.RelatedTopic;
018    import de.deepamehta.core.Topic;
019    import de.deepamehta.core.TopicType;
020    import de.deepamehta.core.Type;
021    import de.deepamehta.core.ViewConfiguration;
022    import de.deepamehta.core.model.ChildTopicsModel;
023    import de.deepamehta.core.model.SimpleValue;
024    import de.deepamehta.core.model.TopicModel;
025    import de.deepamehta.core.osgi.PluginActivator;
026    import de.deepamehta.core.service.DeepaMehtaEvent;
027    import de.deepamehta.core.service.EventListener;
028    import de.deepamehta.core.service.Inject;
029    import de.deepamehta.core.service.Transactional;
030    import de.deepamehta.core.service.event.AllPluginsActiveListener;
031    import de.deepamehta.core.service.event.IntroduceTopicTypeListener;
032    import de.deepamehta.core.service.event.IntroduceAssociationTypeListener;
033    import de.deepamehta.core.service.event.PostCreateAssociationListener;
034    import de.deepamehta.core.service.event.PostCreateTopicListener;
035    import de.deepamehta.core.service.event.PostUpdateTopicListener;
036    import de.deepamehta.core.service.event.PreSendAssociationTypeListener;
037    import de.deepamehta.core.service.event.PreSendTopicTypeListener;
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.DeepaMehtaUtils;
042    import de.deepamehta.core.util.JavaUtils;
043    
044    import org.codehaus.jettison.json.JSONObject;
045    
046    // ### TODO: hide Jersey internals. Move to JAX-RS 2.0.
047    import com.sun.jersey.spi.container.ContainerRequest;
048    
049    import javax.servlet.http.HttpServletRequest;
050    import javax.servlet.http.HttpServletResponse;
051    import javax.servlet.http.HttpSession;
052    
053    import javax.ws.rs.GET;
054    import javax.ws.rs.PUT;
055    import javax.ws.rs.POST;
056    import javax.ws.rs.DELETE;
057    import javax.ws.rs.Consumes;
058    import javax.ws.rs.Path;
059    import javax.ws.rs.PathParam;
060    import javax.ws.rs.Produces;
061    import javax.ws.rs.WebApplicationException;
062    import javax.ws.rs.core.Context;
063    import javax.ws.rs.core.Response;
064    import javax.ws.rs.core.Response.Status;
065    
066    import java.util.Collection;
067    import java.util.Enumeration;
068    import java.util.List;
069    import java.util.logging.Logger;
070    
071    
072    
073    @Path("/accesscontrol")
074    @Consumes("application/json")
075    @Produces("application/json")
076    public class AccessControlPlugin extends PluginActivator implements AccessControlService, AllPluginsActiveListener,
077                                                                                           PostCreateTopicListener,
078                                                                                           PostCreateAssociationListener,
079                                                                                           PostUpdateTopicListener,
080                                                                                           IntroduceTopicTypeListener,
081                                                                                           IntroduceAssociationTypeListener,
082                                                                                           ServiceRequestFilterListener,
083                                                                                           ResourceRequestFilterListener,
084                                                                                           PreSendTopicTypeListener,
085                                                                                           PreSendAssociationTypeListener {
086    
087        // ------------------------------------------------------------------------------------------------------- Constants
088    
089        // Security settings
090        private static final boolean READ_REQUIRES_LOGIN  = Boolean.getBoolean("dm4.security.read_requires_login");
091        private static final boolean WRITE_REQUIRES_LOGIN = Boolean.getBoolean("dm4.security.write_requires_login");
092        private static final String SUBNET_FILTER         = System.getProperty("dm4.security.subnet_filter");
093    
094        private static final String AUTHENTICATION_REALM = "DeepaMehta";
095    
096        // Default user account
097        private static final String DEFAULT_USERNAME = "admin";
098        private static final String DEFAULT_PASSWORD = "";
099    
100        // Default ACLs
101        private static final AccessControlList DEFAULT_INSTANCE_ACL = new AccessControlList(
102            new ACLEntry(Operation.WRITE,  UserRole.CREATOR, UserRole.OWNER, UserRole.MEMBER)
103        );
104        private static final AccessControlList DEFAULT_TYPE_ACL = new AccessControlList(
105            new ACLEntry(Operation.WRITE,  UserRole.CREATOR, UserRole.OWNER, UserRole.MEMBER),
106            new ACLEntry(Operation.CREATE, UserRole.CREATOR, UserRole.OWNER, UserRole.MEMBER)
107        );
108        //
109        private static final AccessControlList DEFAULT_USER_ACCOUNT_ACL = new AccessControlList(
110            new ACLEntry(Operation.WRITE,  UserRole.CREATOR, UserRole.OWNER)
111        );
112    
113        // Property names
114        private static String URI_CREATOR = "dm4.accesscontrol.creator";
115        private static String URI_OWNER = "dm4.accesscontrol.owner";
116        private static String URI_ACL = "dm4.accesscontrol.acl";
117    
118        // Events
119        private static DeepaMehtaEvent POST_LOGIN_USER = new DeepaMehtaEvent(PostLoginUserListener.class) {
120            @Override
121            public void deliver(EventListener listener, Object... params) {
122                ((PostLoginUserListener) listener).postLoginUser(
123                    (String) params[0]
124                );
125            }
126        };
127        private static DeepaMehtaEvent POST_LOGOUT_USER = new DeepaMehtaEvent(PostLogoutUserListener.class) {
128            @Override
129            public void deliver(EventListener listener, Object... params) {
130                ((PostLogoutUserListener) listener).postLogoutUser(
131                    (String) params[0]
132                );
133            }
134        };
135    
136        // ---------------------------------------------------------------------------------------------- Instance Variables
137    
138        @Inject
139        private WorkspacesService wsService;
140    
141        @Context
142        private HttpServletRequest request;
143    
144        private Logger logger = Logger.getLogger(getClass().getName());
145    
146        // -------------------------------------------------------------------------------------------------- Public Methods
147    
148    
149    
150        // *******************************************
151        // *** AccessControlService Implementation ***
152        // *******************************************
153    
154    
155    
156        // === Session ===
157    
158        @POST
159        @Path("/login")
160        @Override
161        public void login() {
162            // Note: the actual login is performed by the request filter. See requestFilter().
163        }
164    
165        @POST
166        @Path("/logout")
167        @Override
168        public void logout() {
169            _logout(request);
170            //
171            // For a "private" DeepaMehta installation: emulate a HTTP logout by forcing the webbrowser to bring up its
172            // login dialog and to forget the former Authorization information. The user is supposed to press "Cancel".
173            // The login dialog can't be used to login again.
174            if (READ_REQUIRES_LOGIN) {
175                throw401Unauthorized();
176            }
177        }
178    
179    
180    
181        // === User ===
182    
183        @GET
184        @Path("/user")
185        @Produces("text/plain")
186        @Override
187        public String getUsername() {
188            try {
189                HttpSession session = request.getSession(false);    // create=false
190                if (session == null) {
191                    return null;
192                }
193                return username(session);
194            } catch (IllegalStateException e) {
195                // Note: if not invoked through network no request (and thus no session) is available.
196                // This happens e.g. while starting up.
197                return null;    // user is unknown
198            }
199        }
200    
201        @Override
202        public Topic getUsername(String username) {
203            return dms.getTopic("dm4.accesscontrol.username", new SimpleValue(username));
204        }
205    
206    
207    
208        // === Permissions ===
209    
210        @GET
211        @Path("/topic/{id}")
212        @Override
213        public Permissions getTopicPermissions(@PathParam("id") long topicId) {
214            return getPermissions(dms.getTopic(topicId));
215        }
216    
217        @GET
218        @Path("/association/{id}")
219        @Override
220        public Permissions getAssociationPermissions(@PathParam("id") long assocId) {
221            return getPermissions(dms.getAssociation(assocId));
222        }
223    
224    
225    
226        // === Creator ===
227    
228        @Override
229        public String getCreator(DeepaMehtaObject object) {
230            return object.hasProperty(URI_CREATOR) ? (String) object.getProperty(URI_CREATOR) : null;
231        }
232    
233        @Override
234        public void setCreator(DeepaMehtaObject object, String username) {
235            try {
236                object.setProperty(URI_CREATOR, username, true);    // addToIndex=true
237            } catch (Exception e) {
238                throw new RuntimeException("Setting the creator of " + info(object) + " failed (username=" + username + ")",
239                    e);
240            }
241        }
242    
243    
244    
245        // === Owner ===
246    
247        @Override
248        public String getOwner(DeepaMehtaObject object) {
249            return object.hasProperty(URI_OWNER) ? (String) object.getProperty(URI_OWNER) : null;
250        }
251    
252        @Override
253        public void setOwner(DeepaMehtaObject object, String username) {
254            try {
255                object.setProperty(URI_OWNER, username, true);      // addToIndex=true
256            } catch (Exception e) {
257                throw new RuntimeException("Setting the owner of " + info(object) + " failed (username=" + username + ")",
258                    e);
259            }
260        }
261    
262    
263    
264        // === Access Control List ===
265    
266        @Override
267        public AccessControlList getACL(DeepaMehtaObject object) {
268            try {
269                if (object.hasProperty(URI_ACL)) {
270                    return new AccessControlList(new JSONObject((String) object.getProperty(URI_ACL)));
271                } else {
272                    return new AccessControlList();
273                }
274            } catch (Exception e) {
275                throw new RuntimeException("Fetching the ACL of " + info(object) + " failed", e);
276            }
277        }
278    
279        @Override
280        public void setACL(DeepaMehtaObject object, AccessControlList acl) {
281            try {
282                object.setProperty(URI_ACL, acl.toJSON().toString(), false);    // addToIndex=false
283            } catch (Exception e) {
284                throw new RuntimeException("Setting the ACL of " + info(object) + " failed", e);
285            }
286        }
287    
288    
289    
290        // === Workspaces ===
291    
292        @POST
293        @Path("/user/{username}/workspace/{workspace_id}")
294        @Transactional
295        @Override
296        public void joinWorkspace(@PathParam("username") String username, @PathParam("workspace_id") long workspaceId) {
297            joinWorkspace(getUsername(username), workspaceId);
298        }
299    
300        @Override
301        public void joinWorkspace(Topic username, long workspaceId) {
302            try {
303                wsService.assignToWorkspace(username, workspaceId);
304            } catch (Exception e) {
305                throw new RuntimeException("Joining user " + username + " to workspace " + workspaceId + " failed", e);
306            }
307        }
308    
309    
310    
311        // === Retrieval ===
312    
313        @GET
314        @Path("/creator/{username}/topics")
315        @Override
316        public Collection<Topic> getTopicsByCreator(@PathParam("username") String username) {
317            return dms.getTopicsByProperty(URI_CREATOR, username);
318        }
319    
320        @GET
321        @Path("/owner/{username}/topics")
322        @Override
323        public Collection<Topic> getTopicsByOwner(@PathParam("username") String username) {
324            return dms.getTopicsByProperty(URI_OWNER, username);
325        }
326    
327        @GET
328        @Path("/creator/{username}/assocs")
329        @Override
330        public Collection<Association> getAssociationsByCreator(@PathParam("username") String username) {
331            return dms.getAssociationsByProperty(URI_CREATOR, username);
332        }
333    
334        @GET
335        @Path("/owner/{username}/assocs")
336        @Override
337        public Collection<Association> getAssociationsByOwner(@PathParam("username") String username) {
338            return dms.getAssociationsByProperty(URI_OWNER, username);
339        }
340    
341    
342    
343        // ****************************
344        // *** Hook Implementations ***
345        // ****************************
346    
347    
348    
349        @Override
350        public void postInstall() {
351            logger.info("Creating \"admin\" user account");
352            Topic adminAccount = createUserAccount(new Credentials(DEFAULT_USERNAME, DEFAULT_PASSWORD));
353            // Note 1: the admin account needs to be setup for access control itself.
354            // At post-install time our listeners are not yet registered. So we must setup manually here.
355            // Note 2: at post-install time there is no user session. So we call setupAccessControl() directly
356            // instead of (the higher-level) setupUserAccountAccessControl().
357            setupAccessControl(adminAccount, DEFAULT_USER_ACCOUNT_ACL, DEFAULT_USERNAME);
358            // ### TODO: setup access control for the admin account's Username and Password topics.
359            // However, they are not strictly required for the moment.
360        }
361    
362        @Override
363        public void init() {
364            logger.info("Security settings:" +
365                "\n    dm4.security.read_requires_login=" + READ_REQUIRES_LOGIN +
366                "\n    dm4.security.write_requires_login=" + WRITE_REQUIRES_LOGIN +
367                "\n    dm4.security.subnet_filter=\""+ SUBNET_FILTER + "\"");
368        }
369    
370    
371    
372        // ********************************
373        // *** Listener Implementations ***
374        // ********************************
375    
376    
377    
378        /**
379         * Setup access control for the default user and the default topicmap.
380         *   1) assign default user     to default workspace
381         *   2) assign default topicmap to default workspace
382         *   3) setup access control for default topicmap
383         */
384        @Override
385        public void allPluginsActive() {
386            DeepaMehtaTransaction tx = dms.beginTx();
387            try {
388                // 1) assign default user to default workspace
389                Topic defaultUser = fetchDefaultUser();
390                assignToDefaultWorkspace(defaultUser, "default user (\"admin\")");
391                //
392                Topic defaultTopicmap = fetchDefaultTopicmap();
393                if (defaultTopicmap != null) {
394                    // 2) assign default topicmap to default workspace
395                    assignToDefaultWorkspace(defaultTopicmap, "default topicmap (\"untitled\")");
396                    // 3) setup access control for default topicmap
397                    setupAccessControlForDefaultTopicmap(defaultTopicmap);
398                }
399                //
400                tx.success();
401            } catch (Exception e) {
402                logger.warning("ROLLBACK! (" + this + ")");
403                throw new RuntimeException("Setting up " + this + " failed", e);
404            } finally {
405                tx.finish();
406            }
407        }
408    
409        // ---
410    
411        @Override
412        public void postCreateTopic(Topic topic) {
413            if (isUserAccount(topic)) {
414                setupUserAccountAccessControl(topic);
415            } else {
416                setupDefaultAccessControl(topic);
417            }
418            //
419            // when a workspace is created its creator joins automatically
420            joinIfWorkspace(topic);
421        }
422    
423        @Override
424        public void postCreateAssociation(Association assoc) {
425            setupDefaultAccessControl(assoc);
426        }
427    
428        // ---
429    
430        @Override
431        public void postUpdateTopic(Topic topic, TopicModel newModel, TopicModel oldModel) {
432            if (topic.getTypeUri().equals("dm4.accesscontrol.user_account")) {
433                Topic usernameTopic = topic.getChildTopics().getTopic("dm4.accesscontrol.username");
434                Topic passwordTopic = topic.getChildTopics().getTopic("dm4.accesscontrol.password");
435                String newUsername = usernameTopic.getSimpleValue().toString();
436                TopicModel oldUsernameTopic = oldModel.getChildTopicsModel().getTopic("dm4.accesscontrol.username",
437                    null);
438                String oldUsername = oldUsernameTopic != null ? oldUsernameTopic.getSimpleValue().toString() : "";
439                if (!newUsername.equals(oldUsername)) {
440                    //
441                    if (!oldUsername.equals("")) {
442                        throw new RuntimeException("Changing a Username is not supported (tried \"" + oldUsername +
443                            "\" -> \"" + newUsername + "\")");
444                    }
445                    //
446                    logger.info("### Username has changed from \"" + oldUsername + "\" -> \"" + newUsername +
447                        "\". Setting \"" + newUsername + "\" as the new owner of 3 topics:\n" +
448                        "    - User Account topic (ID " + topic.getId() + ")\n" + 
449                        "    - Username topic (ID " + usernameTopic.getId() + ")\n" + 
450                        "    - Password topic (ID " + passwordTopic.getId() + ")");
451                    setOwner(topic, newUsername);
452                    setOwner(usernameTopic, newUsername);
453                    setOwner(passwordTopic, newUsername);
454                }
455            }
456        }
457    
458        // ---
459    
460        @Override
461        public void introduceTopicType(TopicType topicType) {
462            setupDefaultAccessControl(topicType);
463        }
464    
465        @Override
466        public void introduceAssociationType(AssociationType assocType) {
467            setupDefaultAccessControl(assocType);
468        }
469    
470        // ---
471    
472        @Override
473        public void serviceRequestFilter(ContainerRequest containerRequest) {
474            // Note: we pass the injected HttpServletRequest
475            requestFilter(request);
476        }
477    
478        @Override
479        public void resourceRequestFilter(HttpServletRequest servletRequest) {
480            // Note: for the resource filter no HttpServletRequest is injected
481            requestFilter(servletRequest);
482        }
483    
484        // ---
485    
486        // ### TODO: make the types cachable (like topics/associations). That is, don't deliver the permissions along
487        // with the types (don't use the preSend{}Type hooks). Instead let the client request the permissions separately.
488    
489        @Override
490        public void preSendTopicType(TopicType topicType) {
491            // Note: the permissions for "Meta Meta Type" must be set manually.
492            // This type doesn't exist in DB. Fetching its ACL entries would fail.
493            if (topicType.getUri().equals("dm4.core.meta_meta_type")) {
494                enrichWithPermissions(topicType, createPermissions(false, false));  // write=false, create=false
495                return;
496            }
497            //
498            enrichWithPermissions(topicType, getPermissions(topicType));
499        }
500    
501        @Override
502        public void preSendAssociationType(AssociationType assocType) {
503            enrichWithPermissions(assocType, getPermissions(assocType));
504        }
505    
506    
507    
508        // ------------------------------------------------------------------------------------------------- Private Methods
509    
510        private Topic createUserAccount(Credentials cred) {
511            return dms.createTopic(new TopicModel("dm4.accesscontrol.user_account", new ChildTopicsModel()
512                .put("dm4.accesscontrol.username", cred.username)
513                .put("dm4.accesscontrol.password", cred.password)));
514        }
515    
516        private boolean isUserAccount(Topic topic) {
517            String typeUri = topic.getTypeUri();
518            return typeUri.equals("dm4.accesscontrol.user_account")
519                || typeUri.equals("dm4.accesscontrol.username")
520                || typeUri.equals("dm4.accesscontrol.password");
521        }
522    
523        /**
524         * Fetches the default user ("admin").
525         *
526         * @throws  RuntimeException    If the default user doesn't exist.
527         *
528         * @return  The default user (a Topic of type "Username" / <code>dm4.accesscontrol.username</code>).
529         */
530        private Topic fetchDefaultUser() {
531            return getUsernameOrThrow(DEFAULT_USERNAME);
532        }
533    
534        private Topic getUsernameOrThrow(String username) {
535            Topic usernameTopic = getUsername(username);
536            if (usernameTopic == null) {
537                throw new RuntimeException("User \"" + username + "\" does not exist");
538            }
539            return usernameTopic;
540        }
541    
542        private void joinIfWorkspace(Topic topic) {
543            if (topic.getTypeUri().equals("dm4.workspaces.workspace")) {
544                String username = getUsername();
545                // Note: when the default workspace is created there is no user logged in yet.
546                // The default user is assigned to the default workspace later on (see allPluginsActive()).
547                if (username != null) {
548                    joinWorkspace(username, topic.getId());
549                }
550            }
551        }
552    
553    
554    
555        // === All Plugins Activated ===
556    
557        private void assignToDefaultWorkspace(Topic topic, String info) {
558            String operation = "### Assigning the " + info + " to the default workspace (\"DeepaMehta\")";
559            try {
560                // abort if already assigned
561                List<RelatedTopic> workspaces = wsService.getAssignedWorkspaces(topic);
562                if (workspaces.size() != 0) {
563                    logger.info("### Assigning the " + info + " to a workspace ABORTED -- " +
564                        "already assigned (" + DeepaMehtaUtils.topicNames(workspaces) + ")");
565                    return;
566                }
567                //
568                logger.info(operation);
569                Topic defaultWorkspace = wsService.getDefaultWorkspace();
570                wsService.assignToWorkspace(topic, defaultWorkspace.getId());
571            } catch (Exception e) {
572                throw new RuntimeException(operation + " failed", e);
573            }
574        }
575    
576        private void setupAccessControlForDefaultTopicmap(Topic defaultTopicmap) {
577            String operation = "### Setup access control for the default topicmap (\"untitled\")";
578            try {
579                // Note: we only check for creator assignment.
580                // If an object has a creator assignment it is expected to have an ACL entry as well.
581                if (getCreator(defaultTopicmap) != null) {
582                    logger.info(operation + " ABORTED -- already setup");
583                    return;
584                }
585                //
586                logger.info(operation);
587                setupAccessControl(defaultTopicmap, DEFAULT_INSTANCE_ACL, DEFAULT_USERNAME);
588            } catch (Exception e) {
589                throw new RuntimeException(operation + " failed", e);
590            }
591        }
592    
593        private Topic fetchDefaultTopicmap() {
594            // Note: the Access Control plugin does not DEPEND on the Topicmaps plugin but is designed to work TOGETHER
595            // with the Topicmaps plugin.
596            // Currently the Access Control plugin needs to know some Topicmaps internals e.g. the URI of the default
597            // topicmap. ### TODO: make "optional plugin dependencies" an explicit concept. Plugins must be able to ask
598            // the core weather a certain plugin is installed (regardles weather it is activated already) and would wait
599            // for its service only if installed.
600            return dms.getTopic("uri", new SimpleValue("dm4.topicmaps.default_topicmap"));
601        }
602    
603    
604    
605        // === Request Filter ===
606    
607        private void requestFilter(HttpServletRequest request) {
608            logger.fine("##### " + request.getMethod() + " " + request.getRequestURL() +
609                "\n      ##### \"Authorization\"=\"" + request.getHeader("Authorization") + "\"" +
610                "\n      ##### " + info(request.getSession(false)));    // create=false
611            //
612            checkRequestOrigin(request);    // throws WebApplicationException
613            checkAuthorization(request);    // throws WebApplicationException
614        }
615    
616        // ---
617    
618        private void checkRequestOrigin(HttpServletRequest request) {
619            String remoteAddr = request.getRemoteAddr();
620            boolean allowed = JavaUtils.isInRange(remoteAddr, SUBNET_FILTER);
621            //
622            logger.fine("Remote address=\"" + remoteAddr + "\", dm4.security.subnet_filter=\"" + SUBNET_FILTER +
623                "\" => " + (allowed ? "ALLOWED" : "FORBIDDEN"));
624            //
625            if (!allowed) {
626                throw403Forbidden();    // throws WebApplicationException
627            }
628        }
629    
630        private void checkAuthorization(HttpServletRequest request) {
631            boolean authorized;
632            if (request.getSession(false) != null) {    // create=false
633                authorized = true;
634            } else {
635                String authHeader = request.getHeader("Authorization");
636                if (authHeader != null) {
637                    // Note: if login fails we are NOT authorized, even if no login is required
638                    authorized = tryLogin(new Credentials(authHeader), request);
639                } else {
640                    authorized = !isLoginRequired(request);
641                }
642            }
643            //
644            if (!authorized) {
645                throw401Unauthorized(); // throws WebApplicationException
646            }
647        }
648    
649        // ---
650    
651        private boolean isLoginRequired(HttpServletRequest request) {
652            return request.getMethod().equals("GET") ? READ_REQUIRES_LOGIN : WRITE_REQUIRES_LOGIN;
653        }
654    
655        /**
656         * Checks weather the credentials are valid and if so logs the user in.
657         *
658         * @return  true if the credentials are valid.
659         */
660        private boolean tryLogin(Credentials cred, HttpServletRequest request) {
661            String username = cred.username;
662            if (checkCredentials(cred)) {
663                logger.info("##### Logging in as \"" + username + "\" => SUCCESSFUL!");
664                _login(username, request);
665                return true;
666            } else {
667                logger.info("##### Logging in as \"" + username + "\" => FAILED!");
668                return false;
669            }
670        }
671    
672        private boolean checkCredentials(Credentials cred) {
673            Topic username = getUsername(cred.username);
674            if (username == null) {
675                return false;
676            }
677            return matches(username, cred.password);
678        }
679    
680        // ---
681    
682        private void _login(String username, HttpServletRequest request) {
683            HttpSession session = request.getSession();
684            session.setAttribute("username", username);
685            logger.info("##### Creating new " + info(session));
686            //
687            dms.fireEvent(POST_LOGIN_USER, username);
688        }
689    
690        private void _logout(HttpServletRequest request) {
691            HttpSession session = request.getSession(false);    // create=false
692            String username = username(session);                // save username before invalidating
693            logger.info("##### Logging out from " + info(session));
694            //
695            session.invalidate();
696            //
697            dms.fireEvent(POST_LOGOUT_USER, username);
698        }
699    
700        // ---
701    
702        /**
703         * Prerequisite: username is not <code>null</code>.
704         *
705         * @param   password    The encrypted password.
706         */
707        private boolean matches(Topic username, String password) {
708            return password(fetchUserAccount(username)).equals(password);
709        }
710    
711        /**
712         * Prerequisite: username is not <code>null</code>.
713         */
714        private Topic fetchUserAccount(Topic username) {
715            Topic userAccount = username.getRelatedTopic("dm4.core.composition", "dm4.core.child", "dm4.core.parent",
716                "dm4.accesscontrol.user_account");
717            if (userAccount == null) {
718                throw new RuntimeException("Data inconsistency: there is no User Account topic for username \"" +
719                    username.getSimpleValue() + "\" (username=" + username + ")");
720            }
721            return userAccount;
722        }
723    
724        // ---
725    
726        private String username(HttpSession session) {
727            String username = (String) session.getAttribute("username");
728            if (username == null) {
729                throw new RuntimeException("Session data inconsistency: \"username\" attribute is missing");
730            }
731            return username;
732        }
733    
734        /**
735         * @return  The encryted password of the specified User Account.
736         */
737        private String password(Topic userAccount) {
738            return userAccount.getChildTopics().getString("dm4.accesscontrol.password");
739        }
740    
741        // ---
742    
743        private void throw401Unauthorized() {
744            // Note: a non-private DM installation (read_requires_login=false) utilizes DM's login dialog and must suppress
745            // the browser's login dialog. To suppress the browser's login dialog a contrived authentication scheme "xBasic"
746            // is used (see http://loudvchar.blogspot.ca/2010/11/avoiding-browser-popup-for-401.html)
747            String authScheme = READ_REQUIRES_LOGIN ? "Basic" : "xBasic";
748            throw new WebApplicationException(Response.status(Status.UNAUTHORIZED)
749                .header("WWW-Authenticate", authScheme + " realm=" + AUTHENTICATION_REALM)
750                .header("Content-Type", "text/html")    // for text/plain (default) Safari provides no Web Console
751                .entity("You're not authorized. Sorry.")
752                .build());
753        }
754    
755        private void throw403Forbidden() {
756            throw new WebApplicationException(Response.status(Status.FORBIDDEN)
757                .header("Content-Type", "text/html")    // for text/plain (default) Safari provides no Web Console
758                .entity("Access is forbidden. Sorry.")
759                .build());
760        }
761    
762    
763    
764        // === Create ACL Entries ===
765    
766        /**
767         * Sets the logged in user as the creator and the owner of the specified object
768         * and creates a default access control entry for it.
769         *
770         * If no user is logged in, nothing is performed.
771         */
772        private void setupDefaultAccessControl(DeepaMehtaObject object) {
773            setupAccessControl(object, DEFAULT_INSTANCE_ACL);
774        }
775    
776        private void setupDefaultAccessControl(Type type) {
777            try {
778                String username = getUsername();
779                //
780                if (username == null) {
781                    username = DEFAULT_USERNAME;
782                    setupViewConfigAccessControl(type.getViewConfig());
783                }
784                //
785                setupAccessControl(type, DEFAULT_TYPE_ACL, username);
786            } catch (Exception e) {
787                throw new RuntimeException("Setting up access control for " + info(type) + " failed (" + type + ")", e);
788            }
789        }
790    
791        // ---
792    
793        private void setupUserAccountAccessControl(Topic topic) {
794            setupAccessControl(topic, DEFAULT_USER_ACCOUNT_ACL);
795        }
796    
797        private void setupViewConfigAccessControl(ViewConfiguration viewConfig) {
798            for (Topic configTopic : viewConfig.getConfigTopics()) {
799                setupAccessControl(configTopic, DEFAULT_INSTANCE_ACL, DEFAULT_USERNAME);
800            }
801        }
802    
803        // ---
804    
805        private void setupAccessControl(DeepaMehtaObject object, AccessControlList acl) {
806            try {
807                String username = getUsername();
808                // Note: when no user is logged in we do NOT fallback to the default user for the access control setup.
809                // This would not help in gaining data consistency because the topics/associations created so far
810                // (BEFORE the Access Control plugin is activated) would still have no access control setup.
811                // Note: for types the situation is different. The type-introduction mechanism (see introduceTopicType()
812                // handler above) ensures EVERY type is catched (regardless of plugin activation order). For instances on
813                // the other hand we don't have such a mechanism (and don't want one either).
814                if (username == null) {
815                    logger.fine("Setting up access control for " + info(object) + " ABORTED -- no user is logged in");
816                    return;
817                }
818                //
819                setupAccessControl(object, acl, username);
820            } catch (Exception e) {
821                throw new RuntimeException("Setting up access control for " + info(object) + " failed (" + object + ")", e);
822            }
823        }
824    
825        /**
826         * @param   username    must not be null.
827         */
828        private void setupAccessControl(DeepaMehtaObject object, AccessControlList acl, String username) {
829            setCreator(object, username);
830            setOwner(object, username);
831            setACL(object, acl);
832        }
833    
834    
835    
836        // === Determine Permissions ===
837    
838        private Permissions getPermissions(DeepaMehtaObject object) {
839            return createPermissions(hasPermission(getUsername(), Operation.WRITE, object));
840        }
841    
842        private Permissions getPermissions(Type type) {
843            String username = getUsername();
844            return createPermissions(hasPermission(username, Operation.WRITE, type),
845                                     hasPermission(username, Operation.CREATE, type));
846        }
847    
848        // ---
849    
850        /**
851         * Checks if a user is allowed to perform an operation on an object (topic or association).
852         * If so, <code>true</code> is returned.
853         *
854         * @param   username    the logged in user (a Topic of type "Username" / <code>dm4.accesscontrol.username</code>),
855         *                      or <code>null</code> if no user is logged in.
856         */
857        private boolean hasPermission(String username, Operation operation, DeepaMehtaObject object) {
858            try {
859                logger.fine("Determining permission for " + userInfo(username) + " to " + operation + " " + info(object));
860                UserRole[] userRoles = getACL(object).getUserRoles(operation);
861                for (UserRole userRole : userRoles) {
862                    logger.fine("There is an ACL entry for user role " + userRole);
863                    if (userOccupiesRole(username, userRole, object)) {
864                        logger.fine("=> ALLOWED");
865                        return true;
866                    }
867                }
868                logger.fine("=> DENIED");
869                return false;
870            } catch (Exception e) {
871                throw new RuntimeException("Determining permission for " + info(object) + " failed (" +
872                    userInfo(username) + ", operation=" + operation + ")", e);
873            }
874        }
875    
876        /**
877         * Checks if a user occupies a role with regard to the specified object.
878         * If so, <code>true</code> is returned.
879         *
880         * @param   username    the logged in user (a Topic of type "Username" / <code>dm4.accesscontrol.username</code>),
881         *                      or <code>null</code> if no user is logged in.
882         */
883        private boolean userOccupiesRole(String username, UserRole userRole, DeepaMehtaObject object) {
884            switch (userRole) {
885            case EVERYONE:
886                return true;
887            case USER:
888                return username != null;
889            case MEMBER:
890                return username != null && userIsMember(username, object);
891            case OWNER:
892                return username != null && userIsOwner(username, object);
893            case CREATOR:
894                return username != null && userIsCreator(username, object);
895            default:
896                throw new RuntimeException(userRole + " is an unsupported user role");
897            }
898        }
899    
900        // ---
901    
902        /**
903         * Checks if a user is a member of any workspace the object is assigned to.
904         * If so, <code>true</code> is returned.
905         *
906         * Prerequisite: a user is logged in (<code>username</code> is not <code>null</code>).
907         *
908         * @param   username    a Topic of type "Username" (<code>dm4.accesscontrol.username</code>). ### FIXDOC
909         * @param   object      the object in question.
910         */
911        private boolean userIsMember(String username, DeepaMehtaObject object) {
912            Topic usernameTopic = getUsernameOrThrow(username);
913            List<RelatedTopic> workspaces = wsService.getAssignedWorkspaces(object);
914            logger.fine(info(object) + " is assigned to " + workspaces.size() + " workspaces");
915            for (RelatedTopic workspace : workspaces) {
916                if (wsService.isAssignedToWorkspace(usernameTopic, workspace.getId())) {
917                    logger.fine(userInfo(username) + " IS member of workspace " + workspace);
918                    return true;
919                } else {
920                    logger.fine(userInfo(username) + " is NOT member of workspace " + workspace);
921                }
922            }
923            return false;
924        }
925    
926        /**
927         * Checks if a user is the owner of the object.
928         * If so, <code>true</code> is returned.
929         *
930         * Prerequisite: a user is logged in (<code>username</code> is not <code>null</code>).
931         *
932         * @param   username    a Topic of type "Username" (<code>dm4.accesscontrol.username</code>). ### FIXDOC
933         */
934        private boolean userIsOwner(String username, DeepaMehtaObject object) {
935            String owner = getOwner(object);
936            logger.fine("The owner is " + userInfo(owner));
937            return owner != null && owner.equals(username);
938        }
939    
940        /**
941         * Checks if a user is the creator of the object.
942         * If so, <code>true</code> is returned.
943         *
944         * Prerequisite: a user is logged in (<code>username</code> is not <code>null</code>).
945         *
946         * @param   username    a Topic of type "Username" (<code>dm4.accesscontrol.username</code>). ### FIXDOC
947         */
948        private boolean userIsCreator(String username, DeepaMehtaObject object) {
949            String creator = getCreator(object);
950            logger.fine("The creator is " + userInfo(creator));
951            return creator != null && creator.equals(username);
952        }
953    
954        // ---
955    
956        private void enrichWithPermissions(Type type, Permissions permissions) {
957            // Note: we must extend/override possibly existing permissions.
958            // Consider a type update: directive UPDATE_TOPIC_TYPE is followed by UPDATE_TOPIC, both on the same object.
959            ChildTopicsModel typePermissions = permissions(type);
960            typePermissions.put(Operation.WRITE.uri, permissions.get(Operation.WRITE.uri));
961            typePermissions.put(Operation.CREATE.uri, permissions.get(Operation.CREATE.uri));
962        }
963    
964        private ChildTopicsModel permissions(DeepaMehtaObject object) {
965            // Note 1: "dm4.accesscontrol.permissions" is a contrived URI. There is no such type definition.
966            // Permissions are for transfer only, recalculated for each request, not stored in DB.
967            // Note 2: The permissions topic exists only in the object's model (see note below).
968            // There is no corresponding topic in the attached composite value. So we must query the model here.
969            // (object.getChildTopics().getTopic(...) would not work)
970            TopicModel permissionsTopic = object.getChildTopics().getModel()
971                .getTopic("dm4.accesscontrol.permissions", null);
972            ChildTopicsModel permissions;
973            if (permissionsTopic != null) {
974                permissions = permissionsTopic.getChildTopicsModel();
975            } else {
976                permissions = new ChildTopicsModel();
977                // Note: we put the permissions topic directly in the model here (instead of the attached composite value).
978                // The "permissions" topic is for transfer only. It must not be stored in the DB (as it would when putting
979                // it in the attached composite value).
980                object.getChildTopics().getModel().put("dm4.accesscontrol.permissions", permissions);
981            }
982            return permissions;
983        }
984    
985        // ---
986    
987        private Permissions createPermissions(boolean write) {
988            return new Permissions().add(Operation.WRITE, write);
989        }
990    
991        private Permissions createPermissions(boolean write, boolean create) {
992            return createPermissions(write).add(Operation.CREATE, create);
993        }
994    
995    
996    
997        // === Logging ===
998    
999        private String info(DeepaMehtaObject object) {
1000            if (object instanceof TopicType) {
1001                return "topic type \"" + object.getUri() + "\" (id=" + object.getId() + ")";
1002            } else if (object instanceof AssociationType) {
1003                return "association type \"" + object.getUri() + "\" (id=" + object.getId() + ")";
1004            } else if (object instanceof Topic) {
1005                return "topic " + object.getId() + " (typeUri=\"" + object.getTypeUri() + "\", uri=\"" + object.getUri() +
1006                    "\")";
1007            } else if (object instanceof Association) {
1008                return "association " + object.getId() + " (typeUri=\"" + object.getTypeUri() + "\")";
1009            } else {
1010                throw new RuntimeException("Unexpected object: " + object);
1011            }
1012        }
1013    
1014        private String userInfo(String username) {
1015            return "user " + (username != null ? "\"" + username + "\"" : "<anonymous>");
1016        }
1017    
1018        private String info(HttpSession session) {
1019            return "session" + (session != null ? " " + session.getId() +
1020                " (username=" + username(session) + ")" : ": null");
1021        }
1022    
1023        private String info(HttpServletRequest request) {
1024            StringBuilder info = new StringBuilder();
1025            info.append("    " + request.getMethod() + " " + request.getRequestURI() + "\n");
1026            Enumeration<String> e1 = request.getHeaderNames();
1027            while (e1.hasMoreElements()) {
1028                String name = e1.nextElement();
1029                info.append("\n    " + name + ":");
1030                Enumeration<String> e2 = request.getHeaders(name);
1031                while (e2.hasMoreElements()) {
1032                    String header = e2.nextElement();
1033                    info.append(" " + header);
1034                }
1035            }
1036            return info.toString();
1037        }
1038    }