001 package de.deepamehta.plugins.accesscontrol;
002
003 import de.deepamehta.plugins.accesscontrol.model.AccessControlList;
004 import de.deepamehta.plugins.accesscontrol.model.ACLEntry;
005 import de.deepamehta.plugins.accesscontrol.model.Credentials;
006 import de.deepamehta.plugins.accesscontrol.model.Operation;
007 import de.deepamehta.plugins.accesscontrol.model.Permissions;
008 import de.deepamehta.plugins.accesscontrol.model.UserRole;
009 import de.deepamehta.plugins.accesscontrol.service.AccessControlService;
010 import de.deepamehta.plugins.workspaces.service.WorkspacesService;
011
012 import de.deepamehta.core.Association;
013 import de.deepamehta.core.AssociationType;
014 import de.deepamehta.core.DeepaMehtaObject;
015 import de.deepamehta.core.RelatedTopic;
016 import de.deepamehta.core.Topic;
017 import de.deepamehta.core.TopicType;
018 import de.deepamehta.core.Type;
019 import de.deepamehta.core.ViewConfiguration;
020 import de.deepamehta.core.model.CompositeValueModel;
021 import de.deepamehta.core.model.SimpleValue;
022 import de.deepamehta.core.model.TopicModel;
023 import de.deepamehta.core.osgi.PluginActivator;
024 import de.deepamehta.core.service.ClientState;
025 import de.deepamehta.core.service.Directives;
026 import de.deepamehta.core.service.PluginService;
027 import de.deepamehta.core.service.annotation.ConsumesService;
028 import de.deepamehta.core.service.event.AllPluginsActiveListener;
029 import de.deepamehta.core.service.event.IntroduceTopicTypeListener;
030 import de.deepamehta.core.service.event.IntroduceAssociationTypeListener;
031 import de.deepamehta.core.service.event.PostCreateAssociationListener;
032 import de.deepamehta.core.service.event.PostCreateTopicListener;
033 import de.deepamehta.core.service.event.PostUpdateTopicListener;
034 import de.deepamehta.core.service.event.PreSendAssociationTypeListener;
035 import de.deepamehta.core.service.event.PreSendTopicTypeListener;
036 import de.deepamehta.core.service.event.ResourceRequestFilterListener;
037 import de.deepamehta.core.service.event.ServiceRequestFilterListener;
038 import de.deepamehta.core.storage.spi.DeepaMehtaTransaction;
039 import de.deepamehta.core.util.DeepaMehtaUtils;
040 import de.deepamehta.core.util.JavaUtils;
041
042 import org.codehaus.jettison.json.JSONObject;
043
044 // ### TODO: hide Jersey internals. Move to JAX-RS 2.0.
045 import com.sun.jersey.spi.container.ContainerRequest;
046
047 import javax.servlet.http.HttpServletRequest;
048 import javax.servlet.http.HttpServletResponse;
049 import javax.servlet.http.HttpSession;
050
051 import javax.ws.rs.GET;
052 import javax.ws.rs.PUT;
053 import javax.ws.rs.POST;
054 import javax.ws.rs.DELETE;
055 import javax.ws.rs.Consumes;
056 import javax.ws.rs.Path;
057 import javax.ws.rs.PathParam;
058 import javax.ws.rs.Produces;
059 import javax.ws.rs.WebApplicationException;
060 import javax.ws.rs.core.Context;
061 import javax.ws.rs.core.Response;
062 import javax.ws.rs.core.Response.Status;
063
064 import java.util.Collection;
065 import java.util.Enumeration;
066 import java.util.List;
067 import java.util.logging.Logger;
068
069
070
071 @Path("/accesscontrol")
072 @Consumes("application/json")
073 @Produces("application/json")
074 public class AccessControlPlugin extends PluginActivator implements AccessControlService, AllPluginsActiveListener,
075 PostCreateTopicListener,
076 PostCreateAssociationListener,
077 PostUpdateTopicListener,
078 IntroduceTopicTypeListener,
079 IntroduceAssociationTypeListener,
080 ServiceRequestFilterListener,
081 ResourceRequestFilterListener,
082 PreSendTopicTypeListener,
083 PreSendAssociationTypeListener {
084
085 // ------------------------------------------------------------------------------------------------------- Constants
086
087 // Security settings
088 private static final boolean READ_REQUIRES_LOGIN = Boolean.getBoolean("dm4.security.read_requires_login");
089 private static final boolean WRITE_REQUIRES_LOGIN = Boolean.getBoolean("dm4.security.write_requires_login");
090 private static final String SUBNET_FILTER = System.getProperty("dm4.security.subnet_filter");
091
092 private static final String AUTHENTICATION_REALM = "DeepaMehta";
093
094 // Default user account
095 private static final String DEFAULT_USERNAME = "admin";
096 private static final String DEFAULT_PASSWORD = "";
097
098 // Default ACLs
099 private static final AccessControlList DEFAULT_INSTANCE_ACL = new AccessControlList(
100 new ACLEntry(Operation.WRITE, UserRole.CREATOR, UserRole.OWNER, UserRole.MEMBER)
101 );
102 private static final AccessControlList DEFAULT_TYPE_ACL = new AccessControlList(
103 new ACLEntry(Operation.WRITE, UserRole.CREATOR, UserRole.OWNER, UserRole.MEMBER),
104 new ACLEntry(Operation.CREATE, UserRole.CREATOR, UserRole.OWNER, UserRole.MEMBER)
105 );
106 //
107 private static final AccessControlList DEFAULT_USER_ACCOUNT_ACL = new AccessControlList(
108 new ACLEntry(Operation.WRITE, UserRole.CREATOR, UserRole.OWNER)
109 );
110
111 // Property names
112 private static String URI_CREATOR = "dm4.accesscontrol.creator";
113 private static String URI_OWNER = "dm4.accesscontrol.owner";
114 private static String URI_ACL = "dm4.accesscontrol.acl";
115
116 // ---------------------------------------------------------------------------------------------- Instance Variables
117
118 private WorkspacesService wsService;
119
120 @Context
121 private HttpServletRequest request;
122
123 private Logger logger = Logger.getLogger(getClass().getName());
124
125 // -------------------------------------------------------------------------------------------------- Public Methods
126
127
128
129 // *******************************************
130 // *** AccessControlService Implementation ***
131 // *******************************************
132
133
134
135 // === Session ===
136
137 @POST
138 @Path("/login")
139 @Override
140 public void login() {
141 // Note: the actual login is performed by the request filter. See requestFilter().
142 }
143
144 @POST
145 @Path("/logout")
146 @Override
147 public void logout() {
148 HttpSession session = request.getSession(false); // create=false
149 logger.info("##### Logging out from " + info(session));
150 session.invalidate();
151 //
152 // For a "private" DeepaMehta installation: emulate a HTTP logout by forcing the webbrowser to bring up its
153 // login dialog and to forget the former Authorization information. The user is supposed to press "Cancel".
154 // The login dialog can't be used to login again.
155 if (READ_REQUIRES_LOGIN) {
156 throw401Unauthorized();
157 }
158 }
159
160
161
162 // === User ===
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: if not invoked through network no request (and thus no session) is available.
177 // This happens e.g. while starting up.
178 return null; // user is unknown
179 }
180 }
181
182 @Override
183 public Topic getUsername(String username) {
184 return dms.getTopic("dm4.accesscontrol.username", new SimpleValue(username), false);
185 }
186
187
188
189 // === Permissions ===
190
191 @GET
192 @Path("/topic/{id}")
193 @Override
194 public Permissions getTopicPermissions(@PathParam("id") long topicId) {
195 return getPermissions(dms.getTopic(topicId, false));
196 }
197
198 @GET
199 @Path("/association/{id}")
200 @Override
201 public Permissions getAssociationPermissions(@PathParam("id") long assocId) {
202 return getPermissions(dms.getAssociation(assocId, false));
203 }
204
205
206
207 // === Creator ===
208
209 @Override
210 public String getCreator(DeepaMehtaObject object) {
211 return object.hasProperty(URI_CREATOR) ? (String) object.getProperty(URI_CREATOR) : null;
212 }
213
214 @Override
215 public void setCreator(DeepaMehtaObject object, String username) {
216 DeepaMehtaTransaction tx = dms.beginTx();
217 try {
218 object.setProperty(URI_CREATOR, username, true); // addToIndex=true
219 tx.success();
220 } catch (Exception e) {
221 logger.warning("ROLLBACK!");
222 throw new RuntimeException("Setting the creator of object " + object.getId() + " failed", e);
223 } finally {
224 tx.finish();
225 }
226 }
227
228
229
230 // === Owner ===
231
232 @Override
233 public String getOwner(DeepaMehtaObject object) {
234 return object.hasProperty(URI_OWNER) ? (String) object.getProperty(URI_OWNER) : null;
235 }
236
237 @Override
238 public void setOwner(DeepaMehtaObject object, String username) {
239 DeepaMehtaTransaction tx = dms.beginTx();
240 try {
241 object.setProperty(URI_OWNER, username, true); // addToIndex=true
242 tx.success();
243 } catch (Exception e) {
244 logger.warning("ROLLBACK!");
245 throw new RuntimeException("Setting the owner of object " + object.getId() + " failed", e);
246 } finally {
247 tx.finish();
248 }
249 }
250
251
252
253 // === Access Control List ===
254
255 @Override
256 public AccessControlList getACL(DeepaMehtaObject object) {
257 try {
258 if (object.hasProperty(URI_ACL)) {
259 return new AccessControlList(new JSONObject((String) object.getProperty(URI_ACL)));
260 } else {
261 return new AccessControlList();
262 }
263 } catch (Exception e) {
264 throw new RuntimeException("Fetching the ACL of object " + object.getId() + " failed", e);
265 }
266 }
267
268 @Override
269 public void setACL(DeepaMehtaObject object, AccessControlList acl) {
270 DeepaMehtaTransaction tx = dms.beginTx();
271 try {
272 object.setProperty(URI_ACL, acl.toJSON().toString(), false); // addToIndex=false
273 tx.success();
274 } catch (Exception e) {
275 logger.warning("ROLLBACK!");
276 throw new RuntimeException("Setting the ACL of object " + object.getId() + " failed", e);
277 } finally {
278 tx.finish();
279 }
280 }
281
282
283
284 // === Workspaces ===
285
286 @POST
287 @Path("/user/{username}/workspace/{workspace_id}")
288 @Override
289 public void joinWorkspace(@PathParam("username") String username, @PathParam("workspace_id") long workspaceId) {
290 joinWorkspace(getUsername(username), workspaceId);
291 }
292
293 @Override
294 public void joinWorkspace(Topic username, long workspaceId) {
295 try {
296 wsService.assignToWorkspace(username, workspaceId);
297 } catch (Exception e) {
298 throw new RuntimeException("Joining user " + username + " to workspace " + workspaceId + " failed", e);
299 }
300 }
301
302
303
304 // === Retrieval ===
305
306 @GET
307 @Path("/creator/{username}/topics")
308 @Override
309 public Collection<Topic> getTopicsByCreator(@PathParam("username") String username) {
310 return dms.getTopicsByProperty(URI_CREATOR, username);
311 }
312
313 @GET
314 @Path("/owner/{username}/topics")
315 @Override
316 public Collection<Topic> getTopicsByOwner(@PathParam("username") String username) {
317 return dms.getTopicsByProperty(URI_OWNER, username);
318 }
319
320 @GET
321 @Path("/creator/{username}/assocs")
322 @Override
323 public Collection<Association> getAssociationsByCreator(@PathParam("username") String username) {
324 return dms.getAssociationsByProperty(URI_CREATOR, username);
325 }
326
327 @GET
328 @Path("/owner/{username}/assocs")
329 @Override
330 public Collection<Association> getAssociationsByOwner(@PathParam("username") String username) {
331 return dms.getAssociationsByProperty(URI_OWNER, username);
332 }
333
334
335
336 // ****************************
337 // *** Hook Implementations ***
338 // ****************************
339
340
341
342 @Override
343 public void postInstall() {
344 logger.info("Creating \"admin\" user account");
345 Topic adminAccount = createUserAccount(new Credentials(DEFAULT_USERNAME, DEFAULT_PASSWORD));
346 // Note 1: the admin account needs to be setup for access control itself.
347 // At post-install time our listeners are not yet registered. So we must setup manually here.
348 // Note 2: at post-install time there is no user session. So we call setupAccessControl() directly
349 // instead of (the higher-level) setupUserAccountAccessControl().
350 setupAccessControl(adminAccount, DEFAULT_USER_ACCOUNT_ACL, DEFAULT_USERNAME);
351 // ### TODO: setup access control for the admin account's Username and Password topics.
352 // However, they are not strictly required for the moment.
353 }
354
355 @Override
356 public void init() {
357 logger.info("Security settings:" +
358 "\n dm4.security.read_requires_login=" + READ_REQUIRES_LOGIN +
359 "\n dm4.security.write_requires_login=" + WRITE_REQUIRES_LOGIN +
360 "\n dm4.security.subnet_filter=\""+ SUBNET_FILTER + "\"");
361 }
362
363 // ---
364
365 @Override
366 @ConsumesService("de.deepamehta.plugins.workspaces.service.WorkspacesService")
367 public void serviceArrived(PluginService service) {
368 wsService = (WorkspacesService) service;
369 }
370
371 @Override
372 public void serviceGone(PluginService service) {
373 wsService = null;
374 }
375
376
377
378 // ********************************
379 // *** Listener Implementations ***
380 // ********************************
381
382
383
384 /**
385 * Setup access control for the default user and the default topicmap.
386 * 1) assign default user to default workspace
387 * 2) assign default topicmap to default workspace
388 * 3) setup access control for default topicmap
389 */
390 @Override
391 public void allPluginsActive() {
392 // 1) assign default user to default workspace
393 Topic defaultUser = fetchDefaultUser();
394 assignToDefaultWorkspace(defaultUser, "default user (\"admin\")");
395 //
396 Topic defaultTopicmap = fetchDefaultTopicmap();
397 if (defaultTopicmap != null) {
398 // 2) assign default topicmap to default workspace
399 assignToDefaultWorkspace(defaultTopicmap, "default topicmap (\"untitled\")");
400 // 3) setup access control for default topicmap
401 setupAccessControlForDefaultTopicmap(defaultTopicmap);
402 }
403 }
404
405 // ---
406
407 @Override
408 public void postCreateTopic(Topic topic, ClientState clientState, Directives directives) {
409 if (isUserAccount(topic)) {
410 setupUserAccountAccessControl(topic);
411 } else {
412 setupDefaultAccessControl(topic);
413 }
414 //
415 // when a workspace is created its creator joins automatically
416 joinIfWorkspace(topic);
417 }
418
419 @Override
420 public void postCreateAssociation(Association assoc, ClientState clientState, Directives directives) {
421 setupDefaultAccessControl(assoc);
422 }
423
424 // ---
425
426 @Override
427 public void postUpdateTopic(Topic topic, TopicModel newModel, TopicModel oldModel, ClientState clientState,
428 Directives directives) {
429 if (topic.getTypeUri().equals("dm4.accesscontrol.user_account")) {
430 Topic usernameTopic = topic.getCompositeValue().getTopic("dm4.accesscontrol.username");
431 Topic passwordTopic = topic.getCompositeValue().getTopic("dm4.accesscontrol.password");
432 String newUsername = usernameTopic.getSimpleValue().toString();
433 TopicModel oldUsernameTopic = oldModel.getCompositeValueModel().getTopic("dm4.accesscontrol.username",
434 null);
435 String oldUsername = oldUsernameTopic != null ? oldUsernameTopic.getSimpleValue().toString() : "";
436 if (!newUsername.equals(oldUsername)) {
437 //
438 if (!oldUsername.equals("")) {
439 throw new RuntimeException("Changing a Username is not supported (tried \"" + oldUsername +
440 "\" -> \"" + newUsername + "\")");
441 }
442 //
443 logger.info("### Username has changed from \"" + oldUsername + "\" -> \"" + newUsername +
444 "\". Setting \"" + newUsername + "\" as the new owner of 3 topics:\n" +
445 " - User Account topic (ID " + topic.getId() + ")\n" +
446 " - Username topic (ID " + usernameTopic.getId() + ")\n" +
447 " - Password topic (ID " + passwordTopic.getId() + ")");
448 setOwner(topic, newUsername);
449 setOwner(usernameTopic, newUsername);
450 setOwner(passwordTopic, newUsername);
451 }
452 }
453 }
454
455 // ---
456
457 @Override
458 public void introduceTopicType(TopicType topicType, ClientState clientState) {
459 setupDefaultAccessControl(topicType);
460 }
461
462 @Override
463 public void introduceAssociationType(AssociationType assocType, ClientState clientState) {
464 setupDefaultAccessControl(assocType);
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 // ### TODO: make the types cachable (like topics/associations). That is, don't deliver the permissions along
484 // with the types (don't use the preSend{}Type hooks). Instead let the client request the permissions separately.
485
486 @Override
487 public void preSendTopicType(TopicType topicType, ClientState clientState) {
488 // Note: the permissions for "Meta Meta Type" must be set manually.
489 // This type doesn't exist in DB. Fetching its ACL entries would fail.
490 if (topicType.getUri().equals("dm4.core.meta_meta_type")) {
491 enrichWithPermissions(topicType, createPermissions(false, false)); // write=false, create=false
492 return;
493 }
494 //
495 enrichWithPermissions(topicType, getPermissions(topicType));
496 }
497
498 @Override
499 public void preSendAssociationType(AssociationType assocType, ClientState clientState) {
500 enrichWithPermissions(assocType, getPermissions(assocType));
501 }
502
503
504
505 // ------------------------------------------------------------------------------------------------- Private Methods
506
507 private Topic createUserAccount(Credentials cred) {
508 return dms.createTopic(new TopicModel("dm4.accesscontrol.user_account", new CompositeValueModel()
509 .put("dm4.accesscontrol.username", cred.username)
510 .put("dm4.accesscontrol.password", cred.password)), null); // clientState=null
511 }
512
513 private boolean isUserAccount(Topic topic) {
514 String typeUri = topic.getTypeUri();
515 return typeUri.equals("dm4.accesscontrol.user_account")
516 || typeUri.equals("dm4.accesscontrol.username")
517 || typeUri.equals("dm4.accesscontrol.password");
518 }
519
520 /**
521 * Fetches the default user ("admin").
522 *
523 * @throws RuntimeException If the default user doesn't exist.
524 *
525 * @return The default user (a Topic of type "Username" / <code>dm4.accesscontrol.username</code>).
526 */
527 private Topic fetchDefaultUser() {
528 return getUsernameOrThrow(DEFAULT_USERNAME);
529 }
530
531 private Topic getUsernameOrThrow(String username) {
532 Topic usernameTopic = getUsername(username);
533 if (usernameTopic == null) {
534 throw new RuntimeException("User \"" + username + "\" does not exist");
535 }
536 return usernameTopic;
537 }
538
539 private void joinIfWorkspace(Topic topic) {
540 if (topic.getTypeUri().equals("dm4.workspaces.workspace")) {
541 String username = getUsername();
542 // Note: when the default workspace is created there is no user logged in yet.
543 // The default user is assigned to the default workspace later on (see allPluginsActive()).
544 if (username != null) {
545 joinWorkspace(username, topic.getId());
546 }
547 }
548 }
549
550
551
552 // === All Plugins Activated ===
553
554 private void assignToDefaultWorkspace(Topic topic, String info) {
555 String operation = "### Assigning the " + info + " to the default workspace (\"DeepaMehta\")";
556 try {
557 // abort if already assigned
558 List<RelatedTopic> workspaces = wsService.getAssignedWorkspaces(topic);
559 if (workspaces.size() != 0) {
560 logger.info("### Assigning the " + info + " to a workspace ABORTED -- " +
561 "already assigned (" + DeepaMehtaUtils.topicNames(workspaces) + ")");
562 return;
563 }
564 //
565 logger.info(operation);
566 Topic defaultWorkspace = wsService.getDefaultWorkspace();
567 wsService.assignToWorkspace(topic, defaultWorkspace.getId());
568 } catch (Exception e) {
569 throw new RuntimeException(operation + " failed", e);
570 }
571 }
572
573 private void setupAccessControlForDefaultTopicmap(Topic defaultTopicmap) {
574 String operation = "### Setup access control for the default topicmap (\"untitled\")";
575 try {
576 // Note: we only check for creator assignment.
577 // If an object has a creator assignment it is expected to have an ACL entry as well.
578 if (getCreator(defaultTopicmap) != null) {
579 logger.info(operation + " ABORTED -- already setup");
580 return;
581 }
582 //
583 logger.info(operation);
584 setupAccessControl(defaultTopicmap, DEFAULT_INSTANCE_ACL, DEFAULT_USERNAME);
585 } catch (Exception e) {
586 throw new RuntimeException(operation + " failed", e);
587 }
588 }
589
590 private Topic fetchDefaultTopicmap() {
591 // Note: the Access Control plugin does not DEPEND on the Topicmaps plugin but is designed to work TOGETHER
592 // with the Topicmaps plugin.
593 // Currently the Access Control plugin needs to know some Topicmaps internals e.g. the URI of the default
594 // topicmap. ### TODO: make "optional plugin dependencies" an explicit concept. Plugins must be able to ask
595 // the core weather a certain plugin is installed (regardles weather it is activated already) and would wait
596 // for its service only if installed.
597 return dms.getTopic("uri", new SimpleValue("dm4.topicmaps.default_topicmap"), false);
598 }
599
600
601
602 // === Request Filter ===
603
604 private void requestFilter(HttpServletRequest request) {
605 logger.fine("##### " + request.getMethod() + " " + request.getRequestURL() +
606 "\n ##### \"Authorization\"=\"" + request.getHeader("Authorization") + "\"" +
607 "\n ##### " + info(request.getSession(false))); // create=false
608 //
609 checkRequestOrigin(request); // throws WebApplicationException
610 checkAuthorization(request); // throws WebApplicationException
611 }
612
613 // ---
614
615 private void checkRequestOrigin(HttpServletRequest request) {
616 String remoteAddr = request.getRemoteAddr();
617 boolean allowed = JavaUtils.isInRange(remoteAddr, SUBNET_FILTER);
618 //
619 logger.fine("Remote address=\"" + remoteAddr + "\", dm4.security.subnet_filter=\"" + SUBNET_FILTER +
620 "\" => " + (allowed ? "ALLOWED" : "FORBIDDEN"));
621 //
622 if (!allowed) {
623 throw403Forbidden(); // throws WebApplicationException
624 }
625 }
626
627 private void checkAuthorization(HttpServletRequest request) {
628 boolean authorized;
629 if (request.getSession(false) != null) { // create=false
630 authorized = true;
631 } else {
632 String authHeader = request.getHeader("Authorization");
633 if (authHeader != null) {
634 // Note: if login fails we are NOT authorized, even if no login is required
635 authorized = tryLogin(new Credentials(authHeader), request);
636 } else {
637 authorized = !isLoginRequired(request);
638 }
639 }
640 //
641 if (!authorized) {
642 throw401Unauthorized(); // throws WebApplicationException
643 }
644 }
645
646 // ---
647
648 private boolean isLoginRequired(HttpServletRequest request) {
649 return request.getMethod().equals("GET") ? READ_REQUIRES_LOGIN : WRITE_REQUIRES_LOGIN;
650 }
651
652 /**
653 * Checks weather the credentials are valid and if so creates a session.
654 *
655 * @return true if the credentials are valid.
656 */
657 private boolean tryLogin(Credentials cred, HttpServletRequest request) {
658 if (checkCredentials(cred)) {
659 HttpSession session = createSession(cred.username, request);
660 logger.info("##### Logging in as \"" + cred.username + "\" => SUCCESSFUL!" +
661 "\n ##### Creating new " + info(session));
662 return true;
663 } else {
664 logger.info("##### Logging in as \"" + cred.username + "\" => FAILED!");
665 return false;
666 }
667 }
668
669 private boolean checkCredentials(Credentials cred) {
670 Topic username = getUsername(cred.username);
671 if (username == null) {
672 return false;
673 }
674 return matches(username, cred.password);
675 }
676
677 private HttpSession createSession(String username, HttpServletRequest request) {
678 HttpSession session = request.getSession();
679 session.setAttribute("username", username);
680 return session;
681 }
682
683 // ---
684
685 /**
686 * Prerequisite: username is not <code>null</code>.
687 *
688 * @param password The encrypted password.
689 */
690 private boolean matches(Topic username, String password) {
691 return password(fetchUserAccount(username)).equals(password);
692 }
693
694 /**
695 * Prerequisite: username is not <code>null</code>.
696 */
697 private Topic fetchUserAccount(Topic username) {
698 Topic userAccount = username.getRelatedTopic("dm4.core.composition", "dm4.core.child", "dm4.core.parent",
699 "dm4.accesscontrol.user_account", true, false); // fetchComposite=true, fetchRelatingComposite=false
700 if (userAccount == null) {
701 throw new RuntimeException("Data inconsistency: there is no User Account topic for username \"" +
702 username.getSimpleValue() + "\" (username=" + username + ")");
703 }
704 return userAccount;
705 }
706
707 // ---
708
709 private String username(HttpSession session) {
710 String username = (String) session.getAttribute("username");
711 if (username == null) {
712 throw new RuntimeException("Session data inconsistency: \"username\" attribute is missing");
713 }
714 return username;
715 }
716
717 /**
718 * @return The encryted password of the specified User Account.
719 */
720 private String password(Topic userAccount) {
721 return userAccount.getCompositeValue().getString("dm4.accesscontrol.password");
722 }
723
724 // ---
725
726 private void throw401Unauthorized() {
727 // Note: a non-private DM installation (read_requires_login=false) utilizes DM's login dialog and must suppress
728 // the browser's login dialog. To suppress the browser's login dialog a contrived authentication scheme "xBasic"
729 // is used (see http://loudvchar.blogspot.ca/2010/11/avoiding-browser-popup-for-401.html)
730 String authScheme = READ_REQUIRES_LOGIN ? "Basic" : "xBasic";
731 throw new WebApplicationException(Response.status(Status.UNAUTHORIZED)
732 .header("WWW-Authenticate", authScheme + " realm=" + AUTHENTICATION_REALM)
733 .header("Content-Type", "text/html") // for text/plain (default) Safari provides no Web Console
734 .entity("You're not authorized. Sorry.")
735 .build());
736 }
737
738 private void throw403Forbidden() {
739 throw new WebApplicationException(Response.status(Status.FORBIDDEN)
740 .header("Content-Type", "text/html") // for text/plain (default) Safari provides no Web Console
741 .entity("Access is forbidden. Sorry.")
742 .build());
743 }
744
745
746
747 // === Create ACL Entries ===
748
749 /**
750 * Sets the logged in user as the creator and the owner of the specified object
751 * and creates a default access control entry for it.
752 *
753 * If no user is logged in, nothing is performed.
754 */
755 private void setupDefaultAccessControl(DeepaMehtaObject object) {
756 try {
757 String username = getUsername();
758 // Note: when no user is logged in we do NOT fallback to the default user for the access control setup.
759 // This would not help in gaining data consistency because the topics/associations created so far
760 // (BEFORE the Access Control plugin is activated) would still have no access control setup.
761 // Note: for types the situation is different. The type-introduction mechanism (see introduceTopicType()
762 // handler above) ensures EVERY type is catched (regardless of plugin activation order). For instances on
763 // the other hand we don't have such a mechanism (and don't want one either).
764 if (username == null) {
765 logger.fine("Setting up access control for " + info(object) + " ABORTED -- no user is logged in");
766 return;
767 }
768 //
769 setupAccessControl(object, DEFAULT_INSTANCE_ACL, username);
770 } catch (Exception e) {
771 throw new RuntimeException("Setting up access control for " + info(object) + " failed (" + object + ")", e);
772 }
773 }
774
775 private void setupDefaultAccessControl(Type type) {
776 try {
777 String username = getUsername();
778 //
779 if (username == null) {
780 username = DEFAULT_USERNAME;
781 setupViewConfigAccessControl(type.getViewConfig());
782 }
783 //
784 setupAccessControl(type, DEFAULT_TYPE_ACL, username);
785 } catch (Exception e) {
786 throw new RuntimeException("Setting up access control for " + info(type) + " failed (" + type + ")", e);
787 }
788 }
789
790 // ---
791
792 private void setupUserAccountAccessControl(Topic topic) {
793 setupAccessControl(topic, DEFAULT_USER_ACCOUNT_ACL, getUsername());
794 }
795
796 private void setupViewConfigAccessControl(ViewConfiguration viewConfig) {
797 for (Topic configTopic : viewConfig.getConfigTopics()) {
798 setupAccessControl(configTopic, DEFAULT_INSTANCE_ACL, DEFAULT_USERNAME);
799 }
800 }
801
802 // ---
803
804 private void setupAccessControl(DeepaMehtaObject object, AccessControlList acl, String username) {
805 setCreator(object, username);
806 setOwner(object, username);
807 setACL(object, acl);
808 }
809
810
811
812 // === Determine Permissions ===
813
814 private Permissions getPermissions(DeepaMehtaObject object) {
815 return createPermissions(hasPermission(getUsername(), Operation.WRITE, object));
816 }
817
818 private Permissions getPermissions(Type type) {
819 String username = getUsername();
820 return createPermissions(hasPermission(username, Operation.WRITE, type),
821 hasPermission(username, Operation.CREATE, type));
822 }
823
824 // ---
825
826 /**
827 * Checks if a user is allowed to perform an operation on an object (topic or association).
828 * If so, <code>true</code> is returned.
829 *
830 * @param username the logged in user (a Topic of type "Username" / <code>dm4.accesscontrol.username</code>),
831 * or <code>null</code> if no user is logged in.
832 */
833 private boolean hasPermission(String username, Operation operation, DeepaMehtaObject object) {
834 try {
835 logger.fine("Determining permission for " + userInfo(username) + " to " + operation + " " + info(object));
836 UserRole[] userRoles = getACL(object).getUserRoles(operation);
837 for (UserRole userRole : userRoles) {
838 logger.fine("There is an ACL entry for user role " + userRole);
839 if (userOccupiesRole(username, userRole, object)) {
840 logger.fine("=> ALLOWED");
841 return true;
842 }
843 }
844 logger.fine("=> DENIED");
845 return false;
846 } catch (Exception e) {
847 throw new RuntimeException("Determining permission for " + info(object) + " failed (" +
848 userInfo(username) + ", operation=" + operation + ")", e);
849 }
850 }
851
852 /**
853 * Checks if a user occupies a role with regard to the specified object.
854 * If so, <code>true</code> is returned.
855 *
856 * @param username the logged in user (a Topic of type "Username" / <code>dm4.accesscontrol.username</code>),
857 * or <code>null</code> if no user is logged in.
858 */
859 private boolean userOccupiesRole(String username, UserRole userRole, DeepaMehtaObject object) {
860 switch (userRole) {
861 case EVERYONE:
862 return true;
863 case USER:
864 return username != null;
865 case MEMBER:
866 return username != null && userIsMember(username, object);
867 case OWNER:
868 return username != null && userIsOwner(username, object);
869 case CREATOR:
870 return username != null && userIsCreator(username, object);
871 default:
872 throw new RuntimeException(userRole + " is an unsupported user role");
873 }
874 }
875
876 // ---
877
878 /**
879 * Checks if a user is a member of any workspace the object is assigned to.
880 * If so, <code>true</code> is returned.
881 *
882 * Prerequisite: a user is logged in (<code>username</code> is not <code>null</code>).
883 *
884 * @param username a Topic of type "Username" (<code>dm4.accesscontrol.username</code>). ### FIXDOC
885 * @param object the object in question.
886 */
887 private boolean userIsMember(String username, DeepaMehtaObject object) {
888 Topic usernameTopic = getUsernameOrThrow(username);
889 List<RelatedTopic> workspaces = wsService.getAssignedWorkspaces(object);
890 logger.fine(info(object) + " is assigned to " + workspaces.size() + " workspaces");
891 for (RelatedTopic workspace : workspaces) {
892 if (wsService.isAssignedToWorkspace(usernameTopic, workspace.getId())) {
893 logger.fine(userInfo(username) + " IS member of workspace " + workspace);
894 return true;
895 } else {
896 logger.fine(userInfo(username) + " is NOT member of workspace " + workspace);
897 }
898 }
899 return false;
900 }
901
902 /**
903 * Checks if a user is the owner of the object.
904 * If so, <code>true</code> is returned.
905 *
906 * Prerequisite: a user is logged in (<code>username</code> is not <code>null</code>).
907 *
908 * @param username a Topic of type "Username" (<code>dm4.accesscontrol.username</code>). ### FIXDOC
909 */
910 private boolean userIsOwner(String username, DeepaMehtaObject object) {
911 String owner = getOwner(object);
912 logger.fine("The owner is " + userInfo(owner));
913 return owner != null && owner.equals(username);
914 }
915
916 /**
917 * Checks if a user is the creator of the object.
918 * If so, <code>true</code> is returned.
919 *
920 * Prerequisite: a user is logged in (<code>username</code> is not <code>null</code>).
921 *
922 * @param username a Topic of type "Username" (<code>dm4.accesscontrol.username</code>). ### FIXDOC
923 */
924 private boolean userIsCreator(String username, DeepaMehtaObject object) {
925 String creator = getCreator(object);
926 logger.fine("The creator is " + userInfo(creator));
927 return creator != null && creator.equals(username);
928 }
929
930 // ---
931
932 private void enrichWithPermissions(Type type, Permissions permissions) {
933 // Note: we must extend/override possibly existing permissions.
934 // Consider a type update: directive UPDATE_TOPIC_TYPE is followed by UPDATE_TOPIC, both on the same object.
935 CompositeValueModel typePermissions = permissions(type);
936 typePermissions.put(Operation.WRITE.uri, permissions.get(Operation.WRITE.uri));
937 typePermissions.put(Operation.CREATE.uri, permissions.get(Operation.CREATE.uri));
938 }
939
940 private CompositeValueModel permissions(DeepaMehtaObject object) {
941 // Note 1: "dm4.accesscontrol.permissions" is a contrived URI. There is no such type definition.
942 // Permissions are for transfer only, recalculated for each request, not stored in DB.
943 // Note 2: The permissions topic exists only in the object's model (see note below).
944 // There is no corresponding topic in the attached composite value. So we must query the model here.
945 // (object.getCompositeValue().getTopic(...) would not work)
946 TopicModel permissionsTopic = object.getCompositeValue().getModel()
947 .getTopic("dm4.accesscontrol.permissions", null);
948 CompositeValueModel permissions;
949 if (permissionsTopic != null) {
950 permissions = permissionsTopic.getCompositeValueModel();
951 } else {
952 permissions = new CompositeValueModel();
953 // Note: we put the permissions topic directly in the model here (instead of the attached composite value).
954 // The "permissions" topic is for transfer only. It must not be stored in the DB (as it would when putting
955 // it in the attached composite value).
956 object.getCompositeValue().getModel().put("dm4.accesscontrol.permissions", permissions);
957 }
958 return permissions;
959 }
960
961 // ---
962
963 private Permissions createPermissions(boolean write) {
964 return new Permissions().add(Operation.WRITE, write);
965 }
966
967 private Permissions createPermissions(boolean write, boolean create) {
968 return createPermissions(write).add(Operation.CREATE, create);
969 }
970
971
972
973 // === Logging ===
974
975 private String info(DeepaMehtaObject object) {
976 if (object instanceof TopicType) {
977 return "topic type \"" + object.getUri() + "\" (id=" + object.getId() + ")";
978 } else if (object instanceof AssociationType) {
979 return "association type \"" + object.getUri() + "\" (id=" + object.getId() + ")";
980 } else if (object instanceof Topic) {
981 return "topic " + object.getId() + " (typeUri=\"" + object.getTypeUri() + "\", uri=\"" + object.getUri() +
982 "\")";
983 } else if (object instanceof Association) {
984 return "association " + object.getId() + " (typeUri=\"" + object.getTypeUri() + "\")";
985 } else {
986 throw new RuntimeException("Unexpected object: " + object);
987 }
988 }
989
990 private String userInfo(String username) {
991 return "user " + (username != null ? "\"" + username + "\"" : "<anonymous>");
992 }
993
994 private String info(HttpSession session) {
995 return "session" + (session != null ? " " + session.getId() +
996 " (username=" + username(session) + ")" : ": null");
997 }
998
999 private String info(HttpServletRequest request) {
1000 StringBuilder info = new StringBuilder();
1001 info.append(" " + request.getMethod() + " " + request.getRequestURI() + "\n");
1002 Enumeration<String> e1 = request.getHeaderNames();
1003 while (e1.hasMoreElements()) {
1004 String name = e1.nextElement();
1005 info.append("\n " + name + ":");
1006 Enumeration<String> e2 = request.getHeaders(name);
1007 while (e2.hasMoreElements()) {
1008 String header = e2.nextElement();
1009 info.append(" " + header);
1010 }
1011 }
1012 return info.toString();
1013 }
1014 }