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                            logger.warning("object " + objectId + " (typeUri=\"" + typeUri +
084                                "\") is not assigned to any workspace -- READ permission is granted");
085                            return true;
086                        case WRITE:
087                            logger.warning("object " + objectId + " (typeUri=\"" + typeUri +
088                                "\") is not assigned to any workspace -- WRITE permission is refused");
089                            return false;
090                        default:
091                            throw new RuntimeException(operation + " is an unsupported operation");
092                        }
093                    }
094                }
095                //
096                return _hasPermission(username, operation, workspaceId);
097            } catch (Exception e) {
098                throw new RuntimeException("Checking permission for object " + objectId + " (typeUri=\"" + typeUri +
099                    "\") failed (" + userInfo(username) + ", operation=" + operation + ")", e);
100            }
101        }
102    
103        @Override
104        public boolean isMember(String username, long workspaceId) {
105            try {
106                if (username == null) {
107                    return false;
108                }
109                // Note: direct storage access is required here
110                AssociationModel membership = dms.storageDecorator.fetchAssociation(TYPE_MEMBERSHIP,
111                    getUsernameTopicOrThrow(username).getId(), workspaceId, "dm4.core.default", "dm4.core.default");
112                return membership != null;
113            } catch (Exception e) {
114                throw new RuntimeException("Checking membership of user \"" + username + "\" and workspace " +
115                    workspaceId + " failed", e);
116            }
117        }
118    
119        @Override
120        public void assignToWorkspace(DeepaMehtaObject object, long workspaceId) {
121            // 1) create assignment association
122            dms.associationFactory(new AssociationModel("dm4.core.aggregation",
123                new TopicRoleModel(object.getId(), "dm4.core.parent"),
124                new TopicRoleModel(workspaceId, "dm4.core.child")
125            ));
126            // 2) store assignment property
127            object.setProperty(PROP_WORKSPACE_ID, workspaceId, false);      // addToIndex=false
128        }
129    
130        // ------------------------------------------------------------------------------------------------- Private Methods
131    
132        /**
133         * Prerequisite: usernameTopic is not <code>null</code>.
134         *
135         * @param   password    The encoded password.
136         */
137        private boolean matches(TopicModel usernameTopic, String password) {
138            return getPassword(getUserAccount(usernameTopic)).equals(password);
139        }
140    
141        /**
142         * Prerequisite: usernameTopic is not <code>null</code>.
143         */
144        private TopicModel getUserAccount(TopicModel usernameTopic) {
145            // Note: checking the credentials is performed by <anonymous> and User Accounts are private.
146            // So direct storage access is required here.
147            RelatedTopicModel userAccount = dms.storageDecorator.fetchTopicRelatedTopic(usernameTopic.getId(),
148                "dm4.core.composition", "dm4.core.child", "dm4.core.parent", "dm4.accesscontrol.user_account");
149            if (userAccount == null) {
150                throw new RuntimeException("Data inconsistency: there is no User Account topic for username \"" +
151                    usernameTopic.getSimpleValue() + "\" (usernameTopic=" + usernameTopic + ")");
152            }
153            return userAccount;
154        }
155    
156        /**
157         * @return  The encoded password of the specified User Account.
158         */
159        private String getPassword(TopicModel userAccount) {
160            // Note: we only have a (User Account) topic model at hand and we don't want instantiate a Topic.
161            // So we use direct storage access here.
162            RelatedTopicModel password = dms.storageDecorator.fetchTopicRelatedTopic(userAccount.getId(),
163                "dm4.core.composition", "dm4.core.parent", "dm4.core.child", "dm4.accesscontrol.password");
164            if (password == null) {
165                throw new RuntimeException("Data inconsistency: there is no Password topic for User Account \"" +
166                    userAccount.getSimpleValue() + "\" (userAccount=" + userAccount + ")");
167            }
168            return password.getSimpleValue().toString();
169        }
170    
171        // ---
172    
173        private boolean _hasPermission(String username, Operation operation, long workspaceId) {
174            switch (operation) {
175            case READ:
176                return hasReadPermission(username, workspaceId);
177            case WRITE:
178                return hasWritePermission(username, workspaceId);
179            default:
180                throw new RuntimeException(operation + " is an unsupported operation");
181            }
182        }
183    
184        // ---
185    
186        /**
187         * @param   username        the logged in user, or <code>null</code> if no user is logged in.
188         * @param   workspaceId     the ID of the workspace that is relevant for the permission check. Is never -1.
189         */
190        private boolean hasReadPermission(String username, long workspaceId) {
191            SharingMode sharingMode = getSharingMode(workspaceId);
192            switch (sharingMode) {
193            case PRIVATE:
194                return isOwner(username, workspaceId);
195            case CONFIDENTIAL:
196                return isOwner(username, workspaceId) || isMember(username, workspaceId);
197            case COLLABORATIVE:
198                return isOwner(username, workspaceId) || isMember(username, workspaceId);
199            case PUBLIC:
200                // Note: the System workspace is special: although it is a public workspace
201                // its content is readable only for logged in users.
202                return workspaceId != getSystemWorkspaceId() || username != null;
203            case COMMON:
204                return true;
205            default:
206                throw new RuntimeException(sharingMode + " is an unsupported sharing mode");
207            }
208        }
209    
210        /**
211         * @param   username        the logged in user, or <code>null</code> if no user is logged in.
212         * @param   workspaceId     the ID of the workspace that is relevant for the permission check. Is never -1.
213         */
214        private boolean hasWritePermission(String username, long workspaceId) {
215            SharingMode sharingMode = getSharingMode(workspaceId);
216            switch (sharingMode) {
217            case PRIVATE:
218                return isOwner(username, workspaceId);
219            case CONFIDENTIAL:
220                return isOwner(username, workspaceId);
221            case COLLABORATIVE:
222                return isOwner(username, workspaceId) || isMember(username, workspaceId);
223            case PUBLIC:
224                return isOwner(username, workspaceId) || isMember(username, workspaceId);
225            case COMMON:
226                return true;
227            default:
228                throw new RuntimeException(sharingMode + " is an unsupported sharing mode");
229            }
230        }
231    
232        // ---
233    
234        // ### TODO: copy in WorkspacesPlugin.java
235        private long getAssignedWorkspaceId(long objectId) {
236            return dms.hasProperty(objectId, PROP_WORKSPACE_ID) ? (Long) dms.getProperty(objectId, PROP_WORKSPACE_ID) : -1;
237        }
238    
239        /**
240         * Checks if a user is the owner of a workspace.
241         *
242         * @param   username    the logged in user, or <code>null</code> if no user is logged in.
243         *
244         * @return  <code>true</code> if the user is the owner, <code>false</code> otherwise.
245         */
246        private boolean isOwner(String username, long workspaceId) {
247            try {
248                if (username == null) {
249                    return false;
250                }
251                return getOwner(workspaceId).equals(username);
252            } catch (Exception e) {
253                throw new RuntimeException("Checking ownership of workspace " + workspaceId + " and user \"" +
254                    username + "\" failed", e);
255            }
256        }
257    
258        private SharingMode getSharingMode(long workspaceId) {
259            // Note: direct storage access is required here
260            TopicModel sharingMode = dms.storageDecorator.fetchTopicRelatedTopic(workspaceId, "dm4.core.aggregation",
261                "dm4.core.parent", "dm4.core.child", "dm4.workspaces.sharing_mode");
262            if (sharingMode == null) {
263                throw new RuntimeException("No sharing mode is assigned to workspace " + workspaceId);
264            }
265            return SharingMode.fromString(sharingMode.getUri());
266        }
267    
268        // ---
269    
270        private String getOwner(long workspaceId) {
271            // Note: direct storage access is required here
272            if (!dms.storageDecorator.hasProperty(workspaceId, PROP_OWNER)) {
273                throw new RuntimeException("No owner is assigned to workspace " + workspaceId);
274            }
275            return (String) dms.storageDecorator.fetchProperty(workspaceId, PROP_OWNER);
276        }
277    
278        private String getTypeUri(long objectId) {
279            // Note: direct storage access is required here
280            return (String) dms.storageDecorator.fetchProperty(objectId, "type_uri");
281        }
282    
283        // ---
284    
285        private TopicModel getUsernameTopic(String username) {
286            // Note: username topics are not readable by <anonymous>.
287            // So direct storage access is required here.
288            return dms.storageDecorator.fetchTopic(TYPE_USERNAME, new SimpleValue(username));
289        }
290    
291        private TopicModel getUsernameTopicOrThrow(String username) {
292            TopicModel usernameTopic = getUsernameTopic(username);
293            if (usernameTopic == null) {
294                throw new RuntimeException("User \"" + username + "\" does not exist");
295            }
296            return usernameTopic;
297        }
298    
299        // ---
300    
301        long getSystemWorkspaceId() {
302            if (systemWorkspaceId != -1) {
303                return systemWorkspaceId;
304            }
305            // Note: fetching the System workspace topic though the Core service would involve a permission check
306            // and run in a vicious circle. So direct storage access is required here.
307            TopicModel workspace = dms.storageDecorator.fetchTopic("uri", new SimpleValue(SYSTEM_WORKSPACE_URI));
308            // Note: the Access Control plugin creates the System workspace before it performs its first permission check.
309            if (workspace == null) {
310                throw new RuntimeException("The System workspace does not exist");
311            }
312            //
313            systemWorkspaceId = workspace.getId();
314            //
315            return systemWorkspaceId;
316        }
317    
318    
319    
320        // === Logging ===
321    
322        // ### TODO: there is a copy in AccessControlPlugin.java
323        private String userInfo(String username) {
324            return "user " + (username != null ? "\"" + username + "\"" : "<anonymous>");
325        }
326    }