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 }