001package de.deepamehta.core.impl;
002
003import de.deepamehta.core.DeepaMehtaObject;
004import de.deepamehta.core.RelatedTopic;
005import de.deepamehta.core.Topic;
006import de.deepamehta.core.model.AssociationModel;
007import de.deepamehta.core.model.SimpleValue;
008import de.deepamehta.core.model.RelatedTopicModel;
009import de.deepamehta.core.model.TopicModel;
010import de.deepamehta.core.model.TopicRoleModel;
011import de.deepamehta.core.service.accesscontrol.AccessControl;
012import de.deepamehta.core.service.accesscontrol.Credentials;
013import de.deepamehta.core.service.accesscontrol.Operation;
014import de.deepamehta.core.service.accesscontrol.SharingMode;
015
016import javax.servlet.http.HttpServletRequest;
017import javax.servlet.http.HttpSession;
018
019import java.util.List;
020import java.util.concurrent.Callable;
021import java.util.logging.Logger;
022
023
024
025class AccessControlImpl implements AccessControl {
026
027    // ------------------------------------------------------------------------------------------------------- Constants
028
029    // Type URIs
030    // ### TODO: move to dm4.core namespace?
031    // ### TODO: copy in AccessControlPlugin.java
032    private static final String TYPE_MEMBERSHIP    = "dm4.accesscontrol.membership";
033    private static final String TYPE_USERNAME      = "dm4.accesscontrol.username";
034    //
035    private static final String TYPE_EMAIL_ADDRESS = "dm4.contacts.email_address";
036    // ### TODO: copy in ConfigPlugin.java
037    private static String ASSOC_TYPE_CONFIGURATION = "dm4.config.configuration";
038    private static String ROLE_TYPE_CONFIGURABLE   = "dm4.config.configurable";
039    private static String ROLE_TYPE_DEFAULT = "dm4.core.default";
040
041    // Property URIs
042    // ### TODO: move to dm4.core namespace?
043    // ### TODO: copy in AccessControlPlugin.java
044    private static final String PROP_OWNER = "dm4.accesscontrol.owner";
045    // ### TODO: copy in WorkspacesPlugin.java
046    private static final String PROP_WORKSPACE_ID = "dm4.workspaces.workspace_id";
047
048    // Workspace URIs
049    // ### TODO: copy in WorkspaceService.java
050    private static final String DEEPAMEHTA_WORKSPACE_URI = "dm4.workspaces.deepamehta";
051    // ### TODO: copy in AccessControlPlugin.java
052    private static final String SYSTEM_WORKSPACE_URI = "dm4.workspaces.system";
053
054    private long systemWorkspaceId = -1;    // initialized lazily
055
056    // ---------------------------------------------------------------------------------------------- Instance Variables
057
058    // standard workspace assignment suppression
059    private ThreadLocal<Integer> suppressionLevel = new ThreadLocal() {
060        @Override
061        protected Integer initialValue() {
062            return 0;
063        }
064    };
065
066    private EmbeddedService dms;
067
068    private Logger logger = Logger.getLogger(getClass().getName());
069
070    // ---------------------------------------------------------------------------------------------------- Constructors
071
072    AccessControlImpl(EmbeddedService dms) {
073        this.dms = dms;
074    }
075
076    // -------------------------------------------------------------------------------------------------- Public Methods
077
078    @Override
079    public Topic checkCredentials(Credentials cred) {
080        TopicModel usernameTopic = null;
081        try {
082            usernameTopic = _getUsernameTopic(cred.username);
083            if (usernameTopic == null) {
084                return null;
085            }
086            if (!matches(usernameTopic, cred.password)) {
087                return null;
088            }
089            return instantiate(usernameTopic);
090        } catch (Exception e) {
091            throw new RuntimeException("Checking credentials for user \"" + cred.username +
092                "\" failed (usernameTopic=" + usernameTopic + ")", e);
093        }
094    }
095
096    @Override
097    public boolean hasPermission(String username, Operation operation, long objectId) {
098        String typeUri = null;
099        try {
100            typeUri = getTypeUri(objectId);
101            long workspaceId;
102            if (typeUri.equals("dm4.workspaces.workspace")) {
103                workspaceId = objectId;
104            } else {
105                workspaceId = getAssignedWorkspaceId(objectId);
106                //
107                if (workspaceId == -1) {
108                    switch (operation) {
109                    case READ:
110                        // ### TODO: remove this workaround
111                        logger.fine("Object " + objectId + " (typeUri=\"" + typeUri +
112                            "\") is not assigned to any workspace -- READ permission is granted");
113                        return true;
114                    case WRITE:
115                        logger.warning("Object " + objectId + " (typeUri=\"" + typeUri +
116                            "\") is not assigned to any workspace -- WRITE permission is refused");
117                        return false;
118                    default:
119                        throw new RuntimeException(operation + " is an unsupported operation");
120                    }
121                }
122            }
123            //
124            return _hasPermission(username, operation, workspaceId);
125        } catch (Exception e) {
126            throw new RuntimeException("Checking permission for object " + objectId + " (typeUri=\"" + typeUri +
127                "\") failed (" + userInfo(username) + ", operation=" + operation + ")", e);
128        }
129    }
130
131
132
133    // === Workspaces / Memberships ===
134
135    @Override
136    public Topic getWorkspace(String uri) {
137        Topic workspace = dms.getTopic("uri", new SimpleValue(uri));
138        if (workspace == null) {
139            throw new RuntimeException("Workspace \"" + uri + "\" does not exist");
140        }
141        return workspace;
142    }
143
144    // ---
145
146    @Override
147    public long getDeepaMehtaWorkspaceId() {
148        return getWorkspace(DEEPAMEHTA_WORKSPACE_URI).getId();
149    }
150
151    @Override
152    public long getSystemWorkspaceId() {
153        if (systemWorkspaceId == -1) {
154            // Note: fetching the System workspace topic though the Core service would involve a permission check
155            // and run in a vicious circle. So direct storage access is required here.
156            TopicModel workspace = fetchTopic("uri", SYSTEM_WORKSPACE_URI);
157            // Note: the Access Control plugin creates the System workspace before it performs its first permission
158            // check.
159            if (workspace == null) {
160                throw new RuntimeException("The System workspace does not exist");
161            }
162            //
163            systemWorkspaceId = workspace.getId();
164        }
165        return systemWorkspaceId;
166    }
167
168    // ---
169
170    @Override
171    public boolean isMember(String username, long workspaceId) {
172        try {
173            if (username == null) {
174                return false;
175            }
176            // Note: direct storage access is required here
177            AssociationModel membership = dms.storageDecorator.fetchAssociation(TYPE_MEMBERSHIP,
178                _getUsernameTopicOrThrow(username).getId(), workspaceId, "dm4.core.default", "dm4.core.default");
179            return membership != null;
180        } catch (Exception e) {
181            throw new RuntimeException("Checking membership of user \"" + username + "\" and workspace " +
182                workspaceId + " failed", e);
183        }
184    }
185
186    @Override
187    public void assignToWorkspace(DeepaMehtaObject object, long workspaceId) {
188        try {
189            // 1) create assignment association
190            dms.associationFactory(new AssociationModel("dm4.core.aggregation",
191                object.getModel().createRoleModel("dm4.core.parent"),
192                new TopicRoleModel(workspaceId, "dm4.core.child")
193            ));
194            // 2) store assignment property
195            object.setProperty(PROP_WORKSPACE_ID, workspaceId, false);      // addToIndex=false
196        } catch (Exception e) {
197            throw new RuntimeException("Assigning " + object + " to workspace " + workspaceId + " failed", e);
198        }
199    }
200
201    // ---
202
203    @Override
204    public <V> V runWithoutWorkspaceAssignment(Callable<V> callable) throws Exception {
205        int level = suppressionLevel.get();
206        try {
207            suppressionLevel.set(level + 1);
208            return callable.call();     // throws exception
209        } finally {
210            suppressionLevel.set(level);
211        }
212    }
213
214    @Override
215    public boolean workspaceAssignmentIsSuppressed() {
216        return suppressionLevel.get() > 0;
217    }
218
219
220
221    // === User Accounts ===
222
223    @Override
224    public Topic getUsernameTopic(String username) {
225        TopicModel usernameTopic = _getUsernameTopic(username);
226        return usernameTopic != null ? instantiate(usernameTopic) : null;
227    }
228
229    @Override
230    public Topic getUsernameTopic(HttpServletRequest request) {
231        String username = getUsername(request);
232        if (username == null) {
233            return null;
234        }
235        return instantiate(_getUsernameTopicOrThrow(username));
236    }
237
238    @Override
239    public String getUsername(HttpServletRequest request) {
240        try {
241            HttpSession session = request.getSession(false);    // create=false
242            if (session == null) {
243                return null;
244            }
245            return username(session);
246        } catch (IllegalStateException e) {
247            // Note: this happens if request is a proxy object (injected by Jersey) and a request method is called
248            // outside request scope. This is the case while system startup.
249            return null;    // user is unknown
250        }
251    }
252
253    @Override
254    public String username(HttpSession session) {
255        String username = (String) session.getAttribute("username");
256        if (username == null) {
257            throw new RuntimeException("Session data inconsistency: \"username\" attribute is missing");
258        }
259        return username;
260    }
261
262    // ---
263
264    @Override
265    public Topic getPrivateWorkspace(String username) {
266        TopicModel passwordTopic = getPasswordTopic(_getUsernameTopicOrThrow(username));
267        long workspaceId = getAssignedWorkspaceId(passwordTopic.getId());
268        if (workspaceId == -1) {
269            throw new RuntimeException("User \"" + username + "\" has no private workspace");
270        }
271        return instantiate(dms.storageDecorator.fetchTopic(workspaceId));
272    }
273
274
275
276    // === Config Service ===
277
278    @Override
279    public RelatedTopic getConfigTopic(String configTypeUri, long topicId) {
280        try {
281            RelatedTopicModel configTopic = dms.storageDecorator.fetchTopicRelatedTopic(topicId,
282                ASSOC_TYPE_CONFIGURATION, ROLE_TYPE_CONFIGURABLE, ROLE_TYPE_DEFAULT, configTypeUri);
283            if (configTopic == null) {
284                throw new RuntimeException("The \"" + configTypeUri + "\" configuration topic for topic " + topicId +
285                    " is missing");
286            }
287            return new AttachedRelatedTopic(configTopic, dms);
288        } catch (Exception e) {
289            throw new RuntimeException("Getting the \"" + configTypeUri + "\" configuration topic for topic " +
290                topicId + " failed", e);
291        }
292    }
293
294
295
296    // === Email Addresses ===
297
298    @Override
299    public boolean emailAddressExists(String emailAddress) {
300        return !queryTopics(TYPE_EMAIL_ADDRESS, emailAddress).isEmpty();
301    }
302
303
304
305    // ------------------------------------------------------------------------------------------------- Private Methods
306
307    /**
308     * Prerequisite: usernameTopic is not <code>null</code>.
309     *
310     * @param   password    The encoded password.
311     */
312    private boolean matches(TopicModel usernameTopic, String password) {
313        String _password = getPasswordTopic(usernameTopic).getSimpleValue().toString();  // encoded
314        return _password.equals(password);
315    }
316
317    /**
318     * Prerequisite: usernameTopic is not <code>null</code>.
319     */
320    private TopicModel getPasswordTopic(TopicModel usernameTopic) {
321        return _getPasswordTopic(_getUserAccount(usernameTopic));
322    }
323
324    /**
325     * Prerequisite: usernameTopic is not <code>null</code>.
326     */
327    private TopicModel _getUserAccount(TopicModel usernameTopic) {
328        // Note: checking the credentials is performed by <anonymous> and User Accounts are private.
329        // So direct storage access is required here.
330        RelatedTopicModel userAccount = dms.storageDecorator.fetchTopicRelatedTopic(usernameTopic.getId(),
331            "dm4.core.composition", "dm4.core.child", "dm4.core.parent", "dm4.accesscontrol.user_account");
332        if (userAccount == null) {
333            throw new RuntimeException("Data inconsistency: there is no User Account topic for username \"" +
334                usernameTopic.getSimpleValue() + "\" (usernameTopic=" + usernameTopic + ")");
335        }
336        return userAccount;
337    }
338
339    /**
340     * Prerequisite: userAccount is not <code>null</code>.
341     */
342    private TopicModel _getPasswordTopic(TopicModel userAccount) {
343        // Note: we only have a (User Account) topic model at hand and we don't want instantiate a Topic.
344        // So we use direct storage access here.
345        RelatedTopicModel password = dms.storageDecorator.fetchTopicRelatedTopic(userAccount.getId(),
346            "dm4.core.composition", "dm4.core.parent", "dm4.core.child", "dm4.accesscontrol.password");
347        if (password == null) {
348            throw new RuntimeException("Data inconsistency: there is no Password topic for User Account \"" +
349                userAccount.getSimpleValue() + "\" (userAccount=" + userAccount + ")");
350        }
351        return password;
352    }
353
354    // ---
355
356    private boolean _hasPermission(String username, Operation operation, long workspaceId) {
357        switch (operation) {
358        case READ:
359            return hasReadPermission(username, workspaceId);
360        case WRITE:
361            return hasWritePermission(username, workspaceId);
362        default:
363            throw new RuntimeException(operation + " is an unsupported operation");
364        }
365    }
366
367    // ---
368
369    /**
370     * @param   username        the logged in user, or <code>null</code> if no user is logged in.
371     * @param   workspaceId     the ID of the workspace that is relevant for the permission check. Is never -1.
372     */
373    private boolean hasReadPermission(String username, long workspaceId) {
374        SharingMode sharingMode = getSharingMode(workspaceId);
375        switch (sharingMode) {
376        case PRIVATE:
377            return isOwner(username, workspaceId);
378        case CONFIDENTIAL:
379            return isOwner(username, workspaceId) || isMember(username, workspaceId);
380        case COLLABORATIVE:
381            return isOwner(username, workspaceId) || isMember(username, workspaceId);
382        case PUBLIC:
383            // Note: the System workspace is special: although it is a public workspace
384            // its content is readable only for logged in users.
385            return workspaceId != getSystemWorkspaceId() || username != null;
386        case COMMON:
387            return true;
388        default:
389            throw new RuntimeException(sharingMode + " is an unsupported sharing mode");
390        }
391    }
392
393    /**
394     * @param   username        the logged in user, or <code>null</code> if no user is logged in.
395     * @param   workspaceId     the ID of the workspace that is relevant for the permission check. Is never -1.
396     */
397    private boolean hasWritePermission(String username, long workspaceId) {
398        SharingMode sharingMode = getSharingMode(workspaceId);
399        switch (sharingMode) {
400        case PRIVATE:
401            return isOwner(username, workspaceId);
402        case CONFIDENTIAL:
403            return isOwner(username, workspaceId);
404        case COLLABORATIVE:
405            return isOwner(username, workspaceId) || isMember(username, workspaceId);
406        case PUBLIC:
407            return isOwner(username, workspaceId) || isMember(username, workspaceId);
408        case COMMON:
409            return true;
410        default:
411            throw new RuntimeException(sharingMode + " is an unsupported sharing mode");
412        }
413    }
414
415    // ---
416
417    // ### TODO: copy in WorkspacesPlugin.java
418    private long getAssignedWorkspaceId(long objectId) {
419        return dms.hasProperty(objectId, PROP_WORKSPACE_ID) ? (Long) dms.getProperty(objectId, PROP_WORKSPACE_ID) : -1;
420    }
421
422    /**
423     * Checks if a user is the owner of a workspace.
424     *
425     * @param   username    the logged in user, or <code>null</code> if no user is logged in.
426     *
427     * @return  <code>true</code> if the user is the owner, <code>false</code> otherwise.
428     */
429    private boolean isOwner(String username, long workspaceId) {
430        try {
431            if (username == null) {
432                return false;
433            }
434            return getOwner(workspaceId).equals(username);
435        } catch (Exception e) {
436            throw new RuntimeException("Checking ownership of workspace " + workspaceId + " and user \"" +
437                username + "\" failed", e);
438        }
439    }
440
441    private SharingMode getSharingMode(long workspaceId) {
442        // Note: direct storage access is required here
443        TopicModel sharingMode = dms.storageDecorator.fetchTopicRelatedTopic(workspaceId, "dm4.core.aggregation",
444            "dm4.core.parent", "dm4.core.child", "dm4.workspaces.sharing_mode");
445        if (sharingMode == null) {
446            throw new RuntimeException("No sharing mode is assigned to workspace " + workspaceId);
447        }
448        return SharingMode.fromString(sharingMode.getUri());
449    }
450
451    // ---
452
453    private String getOwner(long workspaceId) {
454        // Note: direct storage access is required here
455        if (!dms.storageDecorator.hasProperty(workspaceId, PROP_OWNER)) {
456            throw new RuntimeException("No owner is assigned to workspace " + workspaceId);
457        }
458        return (String) dms.storageDecorator.fetchProperty(workspaceId, PROP_OWNER);
459    }
460
461    private String getTypeUri(long objectId) {
462        // Note: direct storage access is required here
463        return (String) dms.storageDecorator.fetchProperty(objectId, "type_uri");
464    }
465
466    // ---
467
468    private TopicModel _getUsernameTopic(String username) {
469        // Note: username topics are not readable by <anonymous>.
470        // So direct storage access is required here.
471        return fetchTopic(TYPE_USERNAME, username);
472    }
473
474    private TopicModel _getUsernameTopicOrThrow(String username) {
475        TopicModel usernameTopic = _getUsernameTopic(username);
476        if (usernameTopic == null) {
477            throw new RuntimeException("User \"" + username + "\" does not exist");
478        }
479        return usernameTopic;
480    }
481
482    // ---
483
484    /**
485     * Fetches a topic by key/value via direct storage access.
486     * <p>
487     * IMPORTANT: only applicable to values indexed with <code>dm4.core.key</code>.
488     *
489     * @return  the topic, or <code>null</code> if no such topic exists.
490     */
491    private TopicModel fetchTopic(String key, Object value) {
492        return dms.storageDecorator.fetchTopic(key, new SimpleValue(value));
493    }
494
495    /**
496     * Queries topics by key/value via direct storage access.
497     * <p>
498     * IMPORTANT: only applicable to values indexed with <code>dm4.core.fulltext</code> or
499     * <code>dm4.core.fulltext_key</code>.
500     *
501     * @return  a list, possibly empty.
502     */
503    private List<TopicModel> queryTopics(String key, Object value) {
504        return dms.storageDecorator.queryTopics(key, new SimpleValue(value));
505    }
506
507    // ---
508
509    /**
510     * Instantiates a topic without performing permission check.
511     */
512    private Topic instantiate(TopicModel model) {
513        return new AttachedTopic(model, dms);
514    }
515
516
517
518    // === Logging ===
519
520    // ### TODO: there is a copy in AccessControlPlugin.java
521    private String userInfo(String username) {
522        return "user " + (username != null ? "\"" + username + "\"" : "<anonymous>");
523    }
524}