001 package de.deepamehta.plugins.accesscontrol;
002
003 import de.deepamehta.plugins.accesscontrol.event.PostLoginUserListener;
004 import de.deepamehta.plugins.accesscontrol.event.PostLogoutUserListener;
005 import de.deepamehta.plugins.accesscontrol.service.AccessControlService;
006 import de.deepamehta.plugins.workspaces.service.WorkspacesService;
007
008 import de.deepamehta.core.Association;
009 import de.deepamehta.core.AssociationType;
010 import de.deepamehta.core.ChildTopics;
011 import de.deepamehta.core.DeepaMehtaObject;
012 import de.deepamehta.core.RelatedTopic;
013 import de.deepamehta.core.Topic;
014 import de.deepamehta.core.TopicType;
015 import de.deepamehta.core.Type;
016 import de.deepamehta.core.ViewConfiguration;
017 import de.deepamehta.core.model.AssociationModel;
018 import de.deepamehta.core.model.ChildTopicsModel;
019 import de.deepamehta.core.model.SimpleValue;
020 import de.deepamehta.core.model.TopicModel;
021 import de.deepamehta.core.model.TopicRoleModel;
022 import de.deepamehta.core.osgi.PluginActivator;
023 import de.deepamehta.core.service.DeepaMehtaEvent;
024 import de.deepamehta.core.service.EventListener;
025 import de.deepamehta.core.service.Inject;
026 import de.deepamehta.core.service.Transactional;
027 import de.deepamehta.core.service.accesscontrol.AccessControlException;
028 import de.deepamehta.core.service.accesscontrol.Credentials;
029 import de.deepamehta.core.service.accesscontrol.Operation;
030 import de.deepamehta.core.service.accesscontrol.Permissions;
031 import de.deepamehta.core.service.accesscontrol.SharingMode;
032 import de.deepamehta.core.service.event.PostCreateAssociationListener;
033 import de.deepamehta.core.service.event.PostCreateTopicListener;
034 import de.deepamehta.core.service.event.PostUpdateAssociationListener;
035 import de.deepamehta.core.service.event.PostUpdateTopicListener;
036 import de.deepamehta.core.service.event.PreGetAssociationListener;
037 import de.deepamehta.core.service.event.PreGetTopicListener;
038 import de.deepamehta.core.service.event.ResourceRequestFilterListener;
039 import de.deepamehta.core.service.event.ServiceRequestFilterListener;
040 import de.deepamehta.core.storage.spi.DeepaMehtaTransaction;
041 import de.deepamehta.core.util.JavaUtils;
042
043 // ### TODO: hide Jersey internals. Move to JAX-RS 2.0.
044 import com.sun.jersey.spi.container.ContainerRequest;
045
046 import javax.servlet.http.HttpServletRequest;
047 import javax.servlet.http.HttpSession;
048
049 import javax.ws.rs.GET;
050 import javax.ws.rs.PUT;
051 import javax.ws.rs.POST;
052 import javax.ws.rs.DELETE;
053 import javax.ws.rs.Consumes;
054 import javax.ws.rs.Path;
055 import javax.ws.rs.PathParam;
056 import javax.ws.rs.Produces;
057 import javax.ws.rs.WebApplicationException;
058 import javax.ws.rs.core.Context;
059 import javax.ws.rs.core.Response;
060 import javax.ws.rs.core.Response.Status;
061
062 import java.util.Collection;
063 import java.util.Enumeration;
064 import java.util.logging.Logger;
065
066
067
068 @Path("/accesscontrol")
069 @Consumes("application/json")
070 @Produces("application/json")
071 public class AccessControlPlugin extends PluginActivator implements AccessControlService, PreGetTopicListener,
072 PreGetAssociationListener,
073 PostCreateTopicListener,
074 PostCreateAssociationListener,
075 PostUpdateTopicListener,
076 PostUpdateAssociationListener,
077 ServiceRequestFilterListener,
078 ResourceRequestFilterListener {
079
080 // ------------------------------------------------------------------------------------------------------- Constants
081
082 // Security settings
083 private static final boolean READ_REQUIRES_LOGIN = Boolean.parseBoolean(
084 System.getProperty("dm4.security.read_requires_login", "false"));
085 private static final boolean WRITE_REQUIRES_LOGIN = Boolean.parseBoolean(
086 System.getProperty("dm4.security.write_requires_login", "true"));
087 private static final String SUBNET_FILTER = System.getProperty("dm4.security.subnet_filter", "127.0.0.1/32");
088 // Note: the default values are required in case no config file is in effect. This applies when DM is started
089 // via feature:install from Karaf. The default values must match the values defined in global POM.
090
091 private static final String AUTHENTICATION_REALM = "DeepaMehta";
092
093 // Associations
094 private static final String MEMBERSHIP_TYPE = "dm4.accesscontrol.membership";
095
096 // Property URIs
097 private static String PROP_CREATOR = "dm4.accesscontrol.creator";
098 private static String PROP_OWNER = "dm4.accesscontrol.owner";
099 private static String PROP_MODIFIER = "dm4.accesscontrol.modifier";
100
101 // Events
102 private static DeepaMehtaEvent POST_LOGIN_USER = new DeepaMehtaEvent(PostLoginUserListener.class) {
103 @Override
104 public void deliver(EventListener listener, Object... params) {
105 ((PostLoginUserListener) listener).postLoginUser(
106 (String) params[0]
107 );
108 }
109 };
110 private static DeepaMehtaEvent POST_LOGOUT_USER = new DeepaMehtaEvent(PostLogoutUserListener.class) {
111 @Override
112 public void deliver(EventListener listener, Object... params) {
113 ((PostLogoutUserListener) listener).postLogoutUser(
114 (String) params[0]
115 );
116 }
117 };
118
119 // ---------------------------------------------------------------------------------------------- Instance Variables
120
121 @Inject
122 private WorkspacesService wsService;
123
124 @Context
125 private HttpServletRequest request;
126
127 private Logger logger = Logger.getLogger(getClass().getName());
128
129 // -------------------------------------------------------------------------------------------------- Public Methods
130
131
132
133 // *******************************************
134 // *** AccessControlService Implementation ***
135 // *******************************************
136
137
138
139 // === User Session ===
140
141 @POST
142 @Path("/login")
143 @Override
144 public void login() {
145 // Note: the actual login is performed by the request filter. See requestFilter().
146 }
147
148 @POST
149 @Path("/logout")
150 @Override
151 public void logout() {
152 _logout(request);
153 //
154 // For a "private" DeepaMehta installation: emulate a HTTP logout by forcing the webbrowser to bring up its
155 // login dialog and to forget the former Authorization information. The user is supposed to press "Cancel".
156 // The login dialog can't be used to login again.
157 if (READ_REQUIRES_LOGIN) {
158 throw401Unauthorized();
159 }
160 }
161
162 // ---
163
164 @GET
165 @Path("/user")
166 @Produces("text/plain")
167 @Override
168 public String getUsername() {
169 try {
170 HttpSession session = request.getSession(false); // create=false
171 if (session == null) {
172 return null;
173 }
174 return username(session);
175 } catch (IllegalStateException e) {
176 // Note: this happens if a request method is called outside request scope.
177 // This is the case while system startup.
178 return null; // user is unknown
179 }
180 }
181
182
183
184 // === User Accounts ===
185
186 @POST
187 @Path("/user_account")
188 @Transactional
189 @Override
190 public Topic createUserAccount(Credentials cred) {
191 String username = cred.username;
192 logger.info("Creating user account \"" + username + "\"");
193 //
194 // 1) create user account
195 Topic userAccount = dms.createTopic(new TopicModel("dm4.accesscontrol.user_account", new ChildTopicsModel()
196 .put("dm4.accesscontrol.username", username)
197 .put("dm4.accesscontrol.password", cred.password)));
198 ChildTopics childTopics = userAccount.getChildTopics();
199 Topic usernameTopic = childTopics.getTopic("dm4.accesscontrol.username");
200 Topic passwordTopic = childTopics.getTopic("dm4.accesscontrol.password");
201 //
202 // 2) create private workspace
203 Topic privateWorkspace = wsService.createWorkspace(DEFAULT_PRIVATE_WORKSPACE_NAME, null, SharingMode.PRIVATE);
204 setWorkspaceOwner(privateWorkspace, username);
205 // Note: we don't set a particular creator/modifier here as we don't want suggest that the new user's private
206 // workspace has been created by the new user itself. Instead we set the *current* user as the creator/modifier
207 // (via postCreateTopic() listener). In case of the "admin" user account the creator/modifier remain undefined
208 // as it is actually created by the system itself.
209 //
210 // 3) assign user account and password to private workspace
211 // Note: the current user has no READ access to the private workspace just created.
212 // So we must use the privileged assignToWorkspace calls here (instead of using the Workspaces service).
213 long privateWorkspaceId = privateWorkspace.getId();
214 dms.getAccessControl().assignToWorkspace(userAccount, privateWorkspaceId);
215 dms.getAccessControl().assignToWorkspace(passwordTopic, privateWorkspaceId);
216 //
217 // 4) assign username to "System" workspace
218 Topic systemWorkspace = wsService.getWorkspace(SYSTEM_WORKSPACE_URI);
219 wsService.assignToWorkspace(usernameTopic, systemWorkspace.getId());
220 //
221 return usernameTopic;
222 }
223
224 @GET
225 @Path("/user/workspace")
226 @Override
227 public Topic getPrivateWorkspace() {
228 String username = getUsername();
229 if (username == null) {
230 throw new IllegalStateException("No user is logged in");
231 }
232 //
233 Topic passwordTopic = getPasswordTopic(getUserAccount(getUsernameTopic(username)));
234 Topic workspace = wsService.getAssignedWorkspace(passwordTopic.getId());
235 if (workspace == null) {
236 throw new RuntimeException("User \"" + username + "\" has no private workspace");
237 }
238 return workspace;
239 }
240
241 @Override
242 public Topic getUsernameTopic(String username) {
243 return dms.getTopic("dm4.accesscontrol.username", new SimpleValue(username));
244 }
245
246
247
248 // === Workspaces / Memberships ===
249
250 @GET
251 @Path("/workspace/{workspace_id}/owner")
252 @Produces("text/plain")
253 @Override
254 public String getWorkspaceOwner(@PathParam("workspace_id") long workspaceId) {
255 // ### TODO: delegate to Core's AccessControl.getOwner()?
256 return dms.hasProperty(workspaceId, PROP_OWNER) ? (String) dms.getProperty(workspaceId, PROP_OWNER) : null;
257 }
258
259 @Override
260 public void setWorkspaceOwner(Topic workspace, String username) {
261 try {
262 workspace.setProperty(PROP_OWNER, username, true); // addToIndex=true
263 } catch (Exception e) {
264 throw new RuntimeException("Setting the workspace owner of " + info(workspace) + " failed (username=" +
265 username + ")", e);
266 }
267 }
268
269 // ---
270
271 @POST
272 @Path("/user/{username}/workspace/{workspace_id}")
273 @Transactional
274 @Override
275 public void createMembership(@PathParam("username") String username, @PathParam("workspace_id") long workspaceId) {
276 try {
277 dms.createAssociation(new AssociationModel(MEMBERSHIP_TYPE,
278 new TopicRoleModel(getUsernameTopicOrThrow(username).getId(), "dm4.core.default"),
279 new TopicRoleModel(workspaceId, "dm4.core.default")
280 ));
281 } catch (Exception e) {
282 throw new RuntimeException("Creating membership for user \"" + username + "\" and workspace " +
283 workspaceId + " failed", e);
284 }
285 }
286
287 @Override
288 public boolean isMember(String username, long workspaceId) {
289 return dms.getAccessControl().isMember(username, workspaceId);
290 }
291
292
293
294 // === Permissions ===
295
296 @GET
297 @Path("/topic/{id}")
298 @Override
299 public Permissions getTopicPermissions(@PathParam("id") long topicId) {
300 return getPermissions(topicId);
301 }
302
303 @GET
304 @Path("/association/{id}")
305 @Override
306 public Permissions getAssociationPermissions(@PathParam("id") long assocId) {
307 return getPermissions(assocId);
308 }
309
310
311
312 // === Object Info ===
313
314 @GET
315 @Path("/object/{id}/creator")
316 @Produces("text/plain")
317 @Override
318 public String getCreator(@PathParam("id") long objectId) {
319 return dms.hasProperty(objectId, PROP_CREATOR) ? (String) dms.getProperty(objectId, PROP_CREATOR) : null;
320 }
321
322 @GET
323 @Path("/object/{id}/modifier")
324 @Produces("text/plain")
325 @Override
326 public String getModifier(@PathParam("id") long objectId) {
327 return dms.hasProperty(objectId, PROP_MODIFIER) ? (String) dms.getProperty(objectId, PROP_MODIFIER) : null;
328 }
329
330
331
332 // === Retrieval ===
333
334 @GET
335 @Path("/creator/{username}/topics")
336 @Override
337 public Collection<Topic> getTopicsByCreator(@PathParam("username") String username) {
338 return dms.getTopicsByProperty(PROP_CREATOR, username);
339 }
340
341 @GET
342 @Path("/owner/{username}/topics")
343 @Override
344 public Collection<Topic> getTopicsByOwner(@PathParam("username") String username) {
345 return dms.getTopicsByProperty(PROP_OWNER, username);
346 }
347
348 @GET
349 @Path("/creator/{username}/assocs")
350 @Override
351 public Collection<Association> getAssociationsByCreator(@PathParam("username") String username) {
352 return dms.getAssociationsByProperty(PROP_CREATOR, username);
353 }
354
355 @GET
356 @Path("/owner/{username}/assocs")
357 @Override
358 public Collection<Association> getAssociationsByOwner(@PathParam("username") String username) {
359 return dms.getAssociationsByProperty(PROP_OWNER, username);
360 }
361
362
363
364 // ****************************
365 // *** Hook Implementations ***
366 // ****************************
367
368
369
370 @Override
371 public void init() {
372 logger.info("Security settings:" +
373 "\ndm4.security.read_requires_login=" + READ_REQUIRES_LOGIN +
374 "\ndm4.security.write_requires_login=" + WRITE_REQUIRES_LOGIN +
375 "\ndm4.security.subnet_filter=\"" + SUBNET_FILTER + "\"");
376 }
377
378
379
380 // ********************************
381 // *** Listener Implementations ***
382 // ********************************
383
384
385
386 @Override
387 public void preGetTopic(long topicId) {
388 checkReadPermission(topicId);
389 }
390
391 @Override
392 public void preGetAssociation(long assocId) {
393 checkReadPermission(assocId);
394 //
395 long[] playerIds = dms.getPlayerIds(assocId);
396 checkReadPermission(playerIds[0]);
397 checkReadPermission(playerIds[1]);
398 }
399
400 // ---
401
402 @Override
403 public void postCreateTopic(Topic topic) {
404 String typeUri = topic.getTypeUri();
405 if (typeUri.equals("dm4.workspaces.workspace")) {
406 setWorkspaceOwner(topic);
407 } else if (typeUri.equals("dm4.webclient.search")) {
408 assignSearchTopic(topic);
409 }
410 //
411 setCreatorAndModifier(topic);
412 }
413
414 @Override
415 public void postCreateAssociation(Association assoc) {
416 setCreatorAndModifier(assoc);
417 }
418
419 // ---
420
421 // ### TODO: revise/drop this method. Meanwhile a user account is created via dialog.
422 @Override
423 public void postUpdateTopic(Topic topic, TopicModel newModel, TopicModel oldModel) {
424 if (topic.getTypeUri().equals("dm4.accesscontrol.user_account")) {
425 Topic usernameTopic = topic.getChildTopics().getTopic("dm4.accesscontrol.username");
426 Topic passwordTopic = topic.getChildTopics().getTopic("dm4.accesscontrol.password");
427 String newUsername = usernameTopic.getSimpleValue().toString();
428 TopicModel oldUsernameTopic = oldModel.getChildTopicsModel().getTopic("dm4.accesscontrol.username",
429 null);
430 String oldUsername = oldUsernameTopic != null ? oldUsernameTopic.getSimpleValue().toString() : "";
431 if (!newUsername.equals(oldUsername)) {
432 //
433 if (!oldUsername.equals("")) {
434 throw new RuntimeException("Changing a Username is not supported (tried \"" + oldUsername +
435 "\" -> \"" + newUsername + "\")");
436 }
437 //
438 logger.info("### Username has changed from \"" + oldUsername + "\" -> \"" + newUsername +
439 "\". Setting \"" + newUsername + "\" as the new owner of 3 topics:\n" +
440 " - User Account topic (ID " + topic.getId() + ")\n" +
441 " - Username topic (ID " + usernameTopic.getId() + ")\n" +
442 " - Password topic (ID " + passwordTopic.getId() + ")");
443 // ### setOwner(topic, newUsername);
444 // ### setOwner(usernameTopic, newUsername);
445 // ### setOwner(passwordTopic, newUsername);
446 }
447 }
448 //
449 setModifier(topic);
450 }
451
452 @Override
453 public void postUpdateAssociation(Association assoc, AssociationModel oldModel) {
454 if (isMembership(assoc.getModel())) {
455 if (isMembership(oldModel)) {
456 // ### TODO?
457 } else {
458 wsService.assignToWorkspace(assoc, assoc.getTopicByType("dm4.workspaces.workspace").getId());
459 }
460 } else if (isMembership(oldModel)) {
461 // ### TODO?
462 }
463 //
464 setModifier(assoc);
465 }
466
467 // ---
468
469 @Override
470 public void serviceRequestFilter(ContainerRequest containerRequest) {
471 // Note: we pass the injected HttpServletRequest
472 requestFilter(request);
473 }
474
475 @Override
476 public void resourceRequestFilter(HttpServletRequest servletRequest) {
477 // Note: for the resource filter no HttpServletRequest is injected
478 requestFilter(servletRequest);
479 }
480
481
482
483 // ------------------------------------------------------------------------------------------------- Private Methods
484
485 private Topic getUserAccount(Topic usernameTopic) {
486 return usernameTopic.getRelatedTopic("dm4.core.composition", "dm4.core.child", "dm4.core.parent",
487 "dm4.accesscontrol.user_account");
488 }
489
490 private Topic getPasswordTopic(Topic userAccount) {
491 return userAccount.getChildTopics().getTopic("dm4.accesscontrol.password");
492 }
493
494 private Topic getUsernameTopicOrThrow(String username) {
495 Topic usernameTopic = getUsernameTopic(username);
496 if (usernameTopic == null) {
497 throw new RuntimeException("User \"" + username + "\" does not exist");
498 }
499 return usernameTopic;
500 }
501
502 private boolean isMembership(AssociationModel assoc) {
503 return assoc.getTypeUri().equals(MEMBERSHIP_TYPE);
504 }
505
506 private void assignSearchTopic(Topic searchTopic) {
507 try {
508 Topic workspace;
509 if (getUsername() != null) {
510 workspace = getPrivateWorkspace();
511 } else {
512 workspace = wsService.getWorkspace(WorkspacesService.DEEPAMEHTA_WORKSPACE_URI);
513 }
514 wsService.assignToWorkspace(searchTopic, workspace.getId());
515 } catch (Exception e) {
516 throw new RuntimeException("Assigning search topic to workspace failed", e);
517 }
518 }
519
520
521
522 // === Request Filter ===
523
524 private void requestFilter(HttpServletRequest request) {
525 logger.fine("##### " + request.getMethod() + " " + request.getRequestURL() +
526 "\n ##### \"Authorization\"=\"" + request.getHeader("Authorization") + "\"" +
527 "\n ##### " + info(request.getSession(false))); // create=false
528 //
529 checkRequestOrigin(request); // throws WebApplicationException
530 checkAuthorization(request); // throws WebApplicationException
531 }
532
533 // ---
534
535 private void checkRequestOrigin(HttpServletRequest request) {
536 String remoteAddr = request.getRemoteAddr();
537 boolean allowed = JavaUtils.isInRange(remoteAddr, SUBNET_FILTER);
538 //
539 logger.fine("Remote address=\"" + remoteAddr + "\", dm4.security.subnet_filter=\"" + SUBNET_FILTER +
540 "\" => " + (allowed ? "ALLOWED" : "FORBIDDEN"));
541 //
542 if (!allowed) {
543 throw403Forbidden(); // throws WebApplicationException
544 }
545 }
546
547 private void checkAuthorization(HttpServletRequest request) {
548 boolean authorized;
549 if (request.getSession(false) != null) { // create=false
550 authorized = true;
551 } else {
552 String authHeader = request.getHeader("Authorization");
553 if (authHeader != null) {
554 // Note: if login fails we are NOT authorized, even if no login is required
555 authorized = tryLogin(new Credentials(authHeader), request);
556 } else {
557 authorized = !isLoginRequired(request);
558 }
559 }
560 //
561 if (!authorized) {
562 throw401Unauthorized(); // throws WebApplicationException
563 }
564 }
565
566 // ---
567
568 private boolean isLoginRequired(HttpServletRequest request) {
569 return request.getMethod().equals("GET") ? READ_REQUIRES_LOGIN : WRITE_REQUIRES_LOGIN;
570 }
571
572 /**
573 * Checks weather the credentials are valid and if so logs the user in.
574 *
575 * @return true if the credentials are valid.
576 */
577 private boolean tryLogin(Credentials cred, HttpServletRequest request) {
578 String username = cred.username;
579 if (checkCredentials(cred)) {
580 logger.info("##### Logging in as \"" + username + "\" => SUCCESSFUL!");
581 _login(username, request);
582 return true;
583 } else {
584 logger.info("##### Logging in as \"" + username + "\" => FAILED!");
585 return false;
586 }
587 }
588
589 private boolean checkCredentials(Credentials cred) {
590 return dms.getAccessControl().checkCredentials(cred);
591 }
592
593 // ---
594
595 private void _login(String username, HttpServletRequest request) {
596 HttpSession session = request.getSession();
597 session.setAttribute("username", username);
598 logger.info("##### Creating new " + info(session));
599 //
600 dms.fireEvent(POST_LOGIN_USER, username);
601 }
602
603 private void _logout(HttpServletRequest request) {
604 HttpSession session = request.getSession(false); // create=false
605 String username = username(session); // save username before invalidating
606 logger.info("##### Logging out from " + info(session));
607 //
608 session.invalidate();
609 //
610 dms.fireEvent(POST_LOGOUT_USER, username);
611 }
612
613 // ---
614
615 private String username(HttpSession session) {
616 String username = (String) session.getAttribute("username");
617 if (username == null) {
618 throw new RuntimeException("Session data inconsistency: \"username\" attribute is missing");
619 }
620 return username;
621 }
622
623 // ---
624
625 private void throw401Unauthorized() {
626 // Note: a non-private DM installation (read_requires_login=false) utilizes DM's login dialog and must suppress
627 // the browser's login dialog. To suppress the browser's login dialog a contrived authentication scheme "xBasic"
628 // is used (see http://loudvchar.blogspot.ca/2010/11/avoiding-browser-popup-for-401.html)
629 String authScheme = READ_REQUIRES_LOGIN ? "Basic" : "xBasic";
630 throw new WebApplicationException(Response.status(Status.UNAUTHORIZED)
631 .header("WWW-Authenticate", authScheme + " realm=" + AUTHENTICATION_REALM)
632 .header("Content-Type", "text/html") // for text/plain (default) Safari provides no Web Console
633 .entity("You're not authorized. Sorry.")
634 .build());
635 }
636
637 private void throw403Forbidden() {
638 throw new WebApplicationException(Response.status(Status.FORBIDDEN)
639 .header("Content-Type", "text/html") // for text/plain (default) Safari provides no Web Console
640 .entity("Access is forbidden. Sorry.")
641 .build());
642 }
643
644
645
646 // === Setup Access Control ===
647
648 /**
649 * Sets the logged in user as the creator/modifier of the given object.
650 * <p>
651 * If no user is logged in, nothing is performed.
652 */
653 private void setCreatorAndModifier(DeepaMehtaObject object) {
654 try {
655 String username = getUsername();
656 // Note: when no user is logged in we do NOT fallback to the default user for the access control setup.
657 // This would not help in gaining data consistency because the topics/associations created so far
658 // (BEFORE the Access Control plugin is activated) would still have no access control setup.
659 // Note: for types the situation is different. The type-introduction mechanism (see introduceTopicType()
660 // handler above) ensures EVERY type is catched (regardless of plugin activation order). For instances on
661 // the other hand we don't have such a mechanism (and don't want one either).
662 if (username == null) {
663 logger.fine("Setting the creator/modifier of " + info(object) + " ABORTED -- no user is logged in");
664 return;
665 }
666 //
667 setCreatorAndModifier(object, username);
668 } catch (Exception e) {
669 throw new RuntimeException("Setting the creator/modifier of " + info(object) + " failed", e);
670 }
671 }
672
673 /**
674 * @param username must not be null.
675 */
676 private void setCreatorAndModifier(DeepaMehtaObject object, String username) {
677 setCreator(object, username);
678 setModifier(object, username);
679 }
680
681 // ---
682
683 /**
684 * Sets the creator of a topic or an association.
685 */
686 private void setCreator(DeepaMehtaObject object, String username) {
687 try {
688 object.setProperty(PROP_CREATOR, username, true); // addToIndex=true
689 } catch (Exception e) {
690 throw new RuntimeException("Setting the creator of " + info(object) + " failed (username=" + username + ")",
691 e);
692 }
693 }
694
695 // ---
696
697 private void setModifier(DeepaMehtaObject object) {
698 String username = getUsername();
699 // Note: when a plugin topic is updated there is no user logged in yet.
700 if (username == null) {
701 return;
702 }
703 //
704 setModifier(object, username);
705 }
706
707 private void setModifier(DeepaMehtaObject object, String username) {
708 object.setProperty(PROP_MODIFIER, username, false); // addToIndex=false
709 }
710
711 // ---
712
713 private void setWorkspaceOwner(Topic workspace) {
714 String username = getUsername();
715 // Note: username is null if the Access Control plugin is activated already
716 // when a 3rd-party plugin creates a workspace at install-time.
717 if (username == null) {
718 return;
719 }
720 //
721 setWorkspaceOwner(workspace, username);
722 }
723
724
725
726 // === Calculate Permissions ===
727
728 /**
729 * @param objectId a topic ID, or an association ID
730 */
731 private void checkReadPermission(long objectId) {
732 if (!inRequestScope()) {
733 logger.fine("### Object " + objectId + " is accessed by \"System\" -- READ permission is granted");
734 return;
735 }
736 //
737 String username = getUsername();
738 if (!hasPermission(username, Operation.READ, objectId)) {
739 throw new AccessControlException(userInfo(username) + " has no READ permission for object " + objectId);
740 }
741 }
742
743 /**
744 * @param objectId a topic ID, or an association ID.
745 */
746 private Permissions getPermissions(long objectId) {
747 return new Permissions().add(Operation.WRITE, hasPermission(getUsername(), Operation.WRITE, objectId));
748 }
749
750 /**
751 * Checks if a user is permitted to perform an operation on an object (topic or association).
752 *
753 * @param username the logged in user, or <code>null</code> if no user is logged in.
754 * @param objectId a topic ID, or an association ID.
755 *
756 * @return <code>true</code> if permission is granted, <code>false</code> otherwise.
757 */
758 private boolean hasPermission(String username, Operation operation, long objectId) {
759 return dms.getAccessControl().hasPermission(username, operation, objectId);
760 }
761
762 private boolean inRequestScope() {
763 try {
764 request.getMethod();
765 return true;
766 } catch (IllegalStateException e) {
767 // Note: this happens if a request method is called outside request scope.
768 // This is the case while system startup.
769 return false;
770 }
771 }
772
773
774
775 // === Logging ===
776
777 private String info(DeepaMehtaObject object) {
778 if (object instanceof TopicType) {
779 return "topic type \"" + object.getUri() + "\" (id=" + object.getId() + ")";
780 } else if (object instanceof AssociationType) {
781 return "association type \"" + object.getUri() + "\" (id=" + object.getId() + ")";
782 } else if (object instanceof Topic) {
783 return "topic " + object.getId() + " (typeUri=\"" + object.getTypeUri() + "\", uri=\"" + object.getUri() +
784 "\")";
785 } else if (object instanceof Association) {
786 return "association " + object.getId() + " (typeUri=\"" + object.getTypeUri() + "\")";
787 } else {
788 throw new RuntimeException("Unexpected object: " + object);
789 }
790 }
791
792 private String userInfo(String username) {
793 return "user " + (username != null ? "\"" + username + "\"" : "<anonymous>");
794 }
795
796 private String info(HttpSession session) {
797 return "session" + (session != null ? " " + session.getId() +
798 " (username=" + username(session) + ")" : ": null");
799 }
800
801 private String info(HttpServletRequest request) {
802 StringBuilder info = new StringBuilder();
803 info.append(" " + request.getMethod() + " " + request.getRequestURI() + "\n");
804 Enumeration<String> e1 = request.getHeaderNames();
805 while (e1.hasMoreElements()) {
806 String name = e1.nextElement();
807 info.append("\n " + name + ":");
808 Enumeration<String> e2 = request.getHeaders(name);
809 while (e2.hasMoreElements()) {
810 String header = e2.nextElement();
811 info.append(" " + header);
812 }
813 }
814 return info.toString();
815 }
816 }