001    package de.deepamehta.core.impl;
002    
003    import de.deepamehta.core.DeepaMehtaObject;
004    import de.deepamehta.core.Topic;
005    import de.deepamehta.core.model.AssociationModel;
006    import de.deepamehta.core.model.SimpleValue;
007    import de.deepamehta.core.model.RelatedTopicModel;
008    import de.deepamehta.core.model.TopicModel;
009    import de.deepamehta.core.model.TopicRoleModel;
010    import de.deepamehta.core.service.accesscontrol.AccessControl;
011    import de.deepamehta.core.service.accesscontrol.Credentials;
012    import de.deepamehta.core.service.accesscontrol.Operation;
013    import de.deepamehta.core.service.accesscontrol.SharingMode;
014    
015    import java.util.logging.Logger;
016    
017    
018    
019    class AccessControlImpl implements AccessControl {
020    
021        // ------------------------------------------------------------------------------------------------------- Constants
022    
023        // Type URIs
024        // ### TODO: move to dm4.core namespace?
025        // ### TODO: copy in AccessControlPlugin.java
026        private static final String TYPE_MEMBERSHIP = "dm4.accesscontrol.membership";
027        private static final String TYPE_USERNAME   = "dm4.accesscontrol.username";
028    
029        // Property URIs
030        // ### TODO: move to dm4.core namespace?
031        // ### TODO: copy in AccessControlPlugin.java
032        private static final String PROP_OWNER = "dm4.accesscontrol.owner";
033        // ### TODO: copy in WorkspacesPlugin.java
034        private static final String PROP_WORKSPACE_ID = "dm4.workspaces.workspace_id";
035    
036        // ### TODO: copy in AccessControlPlugin.java
037        private static final String SYSTEM_WORKSPACE_URI = "dm4.workspaces.system";
038        private long systemWorkspaceId = -1;    // initialized lazily
039    
040        // ---------------------------------------------------------------------------------------------- Instance Variables
041    
042        private EmbeddedService dms;
043    
044        private Logger logger = Logger.getLogger(getClass().getName());
045    
046        // ---------------------------------------------------------------------------------------------------- Constructors
047    
048        AccessControlImpl(EmbeddedService dms) {
049            this.dms = dms;
050        }
051    
052        // -------------------------------------------------------------------------------------------------- Public Methods
053    
054        @Override
055        public boolean checkCredentials(Credentials cred) {
056            TopicModel usernameTopic = null;
057            try {
058                usernameTopic = getUsernameTopic(cred.username);
059                if (usernameTopic == null) {
060                    return false;
061                }
062                return matches(usernameTopic, cred.password);
063            } catch (Exception e) {
064                throw new RuntimeException("Checking credentials for user \"" + cred.username +
065                    "\" failed (usernameTopic=" + usernameTopic + ")", e);
066            }
067        }
068    
069        @Override
070        public boolean hasPermission(String username, Operation operation, long objectId) {
071            String typeUri = null;
072            try {
073                typeUri = getTypeUri(objectId);
074                long workspaceId;
075                if (typeUri.equals("dm4.workspaces.workspace")) {
076                    workspaceId = objectId;
077                } else {
078                    workspaceId = getAssignedWorkspaceId(objectId);
079                    //
080                    if (workspaceId == -1) {
081                        switch (operation) {
082                        case READ:
083                            // ### TODO: remove this workaround
084                            logger.fine("Object " + objectId + " (typeUri=\"" + typeUri +
085                                "\") is not assigned to any workspace -- READ permission is granted");
086                            return true;
087                        case WRITE:
088                            logger.warning("Object " + objectId + " (typeUri=\"" + typeUri +
089                                "\") is not assigned to any workspace -- WRITE permission is refused");
090                            return false;
091                        default:
092                            throw new RuntimeException(operation + " is an unsupported operation");
093                        }
094                    }
095                }
096                //
097                return _hasPermission(username, operation, workspaceId);
098            } catch (Exception e) {
099                throw new RuntimeException("Checking permission for object " + objectId + " (typeUri=\"" + typeUri +
100                    "\") failed (" + userInfo(username) + ", operation=" + operation + ")", e);
101            }
102        }
103    
104        @Override
105        public boolean isMember(String username, long workspaceId) {
106            try {
107                if (username == null) {
108                    return false;
109                }
110                // Note: direct storage access is required here
111                AssociationModel membership = dms.storageDecorator.fetchAssociation(TYPE_MEMBERSHIP,
112                    getUsernameTopicOrThrow(username).getId(), workspaceId, "dm4.core.default", "dm4.core.default");
113                return membership != null;
114            } catch (Exception e) {
115                throw new RuntimeException("Checking membership of user \"" + username + "\" and workspace " +
116                    workspaceId + " failed", e);
117            }
118        }
119    
120        @Override
121        public void assignToWorkspace(DeepaMehtaObject object, long workspaceId) {
122            // 1) create assignment association
123            dms.associationFactory(new AssociationModel("dm4.core.aggregation",
124                new TopicRoleModel(object.getId(), "dm4.core.parent"),
125                new TopicRoleModel(workspaceId, "dm4.core.child")
126            ));
127            // 2) store assignment property
128            object.setProperty(PROP_WORKSPACE_ID, workspaceId, false);      // addToIndex=false
129        }
130    
131        // ------------------------------------------------------------------------------------------------- Private Methods
132    
133        /**
134         * Prerequisite: usernameTopic is not <code>null</code>.
135         *
136         * @param   password    The encoded password.
137         */
138        private boolean matches(TopicModel usernameTopic, String password) {
139            return getPassword(getUserAccount(usernameTopic)).equals(password);
140        }
141    
142        /**
143         * Prerequisite: usernameTopic is not <code>null</code>.
144         */
145        private TopicModel getUserAccount(TopicModel usernameTopic) {
146            // Note: checking the credentials is performed by <anonymous> and User Accounts are private.
147            // So direct storage access is required here.
148            RelatedTopicModel userAccount = dms.storageDecorator.fetchTopicRelatedTopic(usernameTopic.getId(),
149                "dm4.core.composition", "dm4.core.child", "dm4.core.parent", "dm4.accesscontrol.user_account");
150            if (userAccount == null) {
151                throw new RuntimeException("Data inconsistency: there is no User Account topic for username \"" +
152                    usernameTopic.getSimpleValue() + "\" (usernameTopic=" + usernameTopic + ")");
153            }
154            return userAccount;
155        }
156    
157        /**
158         * @return  The encoded password of the specified User Account.
159         */
160        private String getPassword(TopicModel userAccount) {
161            // Note: we only have a (User Account) topic model at hand and we don't want instantiate a Topic.
162            // So we use direct storage access here.
163            RelatedTopicModel password = dms.storageDecorator.fetchTopicRelatedTopic(userAccount.getId(),
164                "dm4.core.composition", "dm4.core.parent", "dm4.core.child", "dm4.accesscontrol.password");
165            if (password == null) {
166                throw new RuntimeException("Data inconsistency: there is no Password topic for User Account \"" +
167                    userAccount.getSimpleValue() + "\" (userAccount=" + userAccount + ")");
168            }
169            return password.getSimpleValue().toString();
170        }
171    
172        // ---
173    
174        private boolean _hasPermission(String username, Operation operation, long workspaceId) {
175            switch (operation) {
176            case READ:
177                return hasReadPermission(username, workspaceId);
178            case WRITE:
179                return hasWritePermission(username, workspaceId);
180            default:
181                throw new RuntimeException(operation + " is an unsupported operation");
182            }
183        }
184    
185        // ---
186    
187        /**
188         * @param   username        the logged in user, or <code>null</code> if no user is logged in.
189         * @param   workspaceId     the ID of the workspace that is relevant for the permission check. Is never -1.
190         */
191        private boolean hasReadPermission(String username, long workspaceId) {
192            SharingMode sharingMode = getSharingMode(workspaceId);
193            switch (sharingMode) {
194            case PRIVATE:
195                return isOwner(username, workspaceId);
196            case CONFIDENTIAL:
197                return isOwner(username, workspaceId) || isMember(username, workspaceId);
198            case COLLABORATIVE:
199                return isOwner(username, workspaceId) || isMember(username, workspaceId);
200            case PUBLIC:
201                // Note: the System workspace is special: although it is a public workspace
202                // its content is readable only for logged in users.
203                return workspaceId != getSystemWorkspaceId() || username != null;
204            case COMMON:
205                return true;
206            default:
207                throw new RuntimeException(sharingMode + " is an unsupported sharing mode");
208            }
209        }
210    
211        /**
212         * @param   username        the logged in user, or <code>null</code> if no user is logged in.
213         * @param   workspaceId     the ID of the workspace that is relevant for the permission check. Is never -1.
214         */
215        private boolean hasWritePermission(String username, long workspaceId) {
216            SharingMode sharingMode = getSharingMode(workspaceId);
217            switch (sharingMode) {
218            case PRIVATE:
219                return isOwner(username, workspaceId);
220            case CONFIDENTIAL:
221                return isOwner(username, workspaceId);
222            case COLLABORATIVE:
223                return isOwner(username, workspaceId) || isMember(username, workspaceId);
224            case PUBLIC:
225                return isOwner(username, workspaceId) || isMember(username, workspaceId);
226            case COMMON:
227                return true;
228            default:
229                throw new RuntimeException(sharingMode + " is an unsupported sharing mode");
230            }
231        }
232    
233        // ---
234    
235        // ### TODO: copy in WorkspacesPlugin.java
236        private long getAssignedWorkspaceId(long objectId) {
237            return dms.hasProperty(objectId, PROP_WORKSPACE_ID) ? (Long) dms.getProperty(objectId, PROP_WORKSPACE_ID) : -1;
238        }
239    
240        /**
241         * Checks if a user is the owner of a workspace.
242         *
243         * @param   username    the logged in user, or <code>null</code> if no user is logged in.
244         *
245         * @return  <code>true</code> if the user is the owner, <code>false</code> otherwise.
246         */
247        private boolean isOwner(String username, long workspaceId) {
248            try {
249                if (username == null) {
250                    return false;
251                }
252                return getOwner(workspaceId).equals(username);
253            } catch (Exception e) {
254                throw new RuntimeException("Checking ownership of workspace " + workspaceId + " and user \"" +
255                    username + "\" failed", e);
256            }
257        }
258    
259        private SharingMode getSharingMode(long workspaceId) {
260            // Note: direct storage access is required here
261            TopicModel sharingMode = dms.storageDecorator.fetchTopicRelatedTopic(workspaceId, "dm4.core.aggregation",
262                "dm4.core.parent", "dm4.core.child", "dm4.workspaces.sharing_mode");
263            if (sharingMode == null) {
264                throw new RuntimeException("No sharing mode is assigned to workspace " + workspaceId);
265            }
266            return SharingMode.fromString(sharingMode.getUri());
267        }
268    
269        // ---
270    
271        private String getOwner(long workspaceId) {
272            // Note: direct storage access is required here
273            if (!dms.storageDecorator.hasProperty(workspaceId, PROP_OWNER)) {
274                throw new RuntimeException("No owner is assigned to workspace " + workspaceId);
275            }
276            return (String) dms.storageDecorator.fetchProperty(workspaceId, PROP_OWNER);
277        }
278    
279        private String getTypeUri(long objectId) {
280            // Note: direct storage access is required here
281            return (String) dms.storageDecorator.fetchProperty(objectId, "type_uri");
282        }
283    
284        // ---
285    
286        private TopicModel getUsernameTopic(String username) {
287            // Note: username topics are not readable by <anonymous>.
288            // So direct storage access is required here.
289            return dms.storageDecorator.fetchTopic(TYPE_USERNAME, new SimpleValue(username));
290        }
291    
292        private TopicModel getUsernameTopicOrThrow(String username) {
293            TopicModel usernameTopic = getUsernameTopic(username);
294            if (usernameTopic == null) {
295                throw new RuntimeException("User \"" + username + "\" does not exist");
296            }
297            return usernameTopic;
298        }
299    
300        // ---
301    
302        long getSystemWorkspaceId() {
303            if (systemWorkspaceId != -1) {
304                return systemWorkspaceId;
305            }
306            // Note: fetching the System workspace topic though the Core service would involve a permission check
307            // and run in a vicious circle. So direct storage access is required here.
308            TopicModel workspace = dms.storageDecorator.fetchTopic("uri", new SimpleValue(SYSTEM_WORKSPACE_URI));
309            // Note: the Access Control plugin creates the System workspace before it performs its first permission check.
310            if (workspace == null) {
311                throw new RuntimeException("The System workspace does not exist");
312            }
313            //
314            systemWorkspaceId = workspace.getId();
315            //
316            return systemWorkspaceId;
317        }
318    
319    
320    
321        // === Logging ===
322    
323        // ### TODO: there is a copy in AccessControlPlugin.java
324        private String userInfo(String username) {
325            return "user " + (username != null ? "\"" + username + "\"" : "<anonymous>");
326        }
327    }