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 }