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