commit 9223bc22159519f2a558f94791c57394b3cd3304
Author: Jakub Danek <jakub.danek@yoso.fi>
Date:   Fri Oct 11 10:15:50 2019 +0200

    re #37 protect getUser and getUsers processes on server-side
    
    Related refactoring - moving relevant logic to UserManager interface
    and to /users API endpoint.

diff --git a/server/src/main/java/org/danekja/ymanager/business/ApiManager.java b/server/src/main/java/org/danekja/ymanager/business/ApiManager.java
index 2f3396f..814e29c 100644
--- a/server/src/main/java/org/danekja/ymanager/business/ApiManager.java
+++ b/server/src/main/java/org/danekja/ymanager/business/ApiManager.java
@@ -1,12 +1,12 @@
 package org.danekja.ymanager.business;
 
 import org.danekja.ymanager.domain.*;
-import org.danekja.ymanager.dto.*;
 import org.danekja.ymanager.dto.DefaultSettings;
+import org.danekja.ymanager.dto.*;
 import org.danekja.ymanager.repository.RequestRepository;
 import org.danekja.ymanager.repository.UserRepository;
 import org.danekja.ymanager.repository.VacationRepository;
-import org.danekja.ymanager.ws.rest.RESTFullException;
+import org.danekja.ymanager.ws.rest.exceptions.RESTFullException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -25,8 +25,6 @@ public class ApiManager implements Manager {
      */
     private static final Logger log = LoggerFactory.getLogger(UserRepository.class);
 
-    private static final int WEEK_LENGTH = 7;
-
     private RequestRepository requestRepository;
     private UserRepository userRepository;
     private VacationRepository vacationRepository;
@@ -38,26 +36,6 @@ public class ApiManager implements Manager {
         this.vacationRepository = vacationRepository;
     }
 
-    @Override
-    public List<BasicProfileUser> getUsers(Status status) throws RESTFullException {
-        try {
-            List<BasicProfileUser> users = userRepository.getAllBasicUsers(status == null ? Status.ACCEPTED : status);
-
-            LocalDate today = LocalDate.now();
-            LocalDate weekBefore = today.minusDays(ApiManager.WEEK_LENGTH);
-            LocalDate weekAfter = today.plusDays(ApiManager.WEEK_LENGTH);
-            for (BasicProfileUser user : users) {
-                user.setCalendar(vacationRepository.getVacationDays(user.getId(), weekBefore, weekAfter));
-            }
-
-            return users;
-
-        } catch (DataAccessException e) {
-            log.error(e.getMessage());
-            throw new RESTFullException(e.getMessage(), "database.error");
-        }
-    }
-
     @Override
     public List<VacationRequest> getVacationRequests(Status status) throws RESTFullException {
         try {
@@ -78,16 +56,6 @@ public class ApiManager implements Manager {
         }
     }
 
-    @Override
-    public FullUserProfile getUserProfile(Long userId) throws RESTFullException {
-        try {
-            return userRepository.getFullUser(userId);
-        } catch (DataAccessException e) {
-            log.error(e.getMessage());
-            throw new RESTFullException(e.getMessage(), "database.error");
-        }
-    }
-
     @Override
     public DefaultSettings getDefaultSettings() throws RESTFullException {
         try {
diff --git a/server/src/main/java/org/danekja/ymanager/business/FileService.java b/server/src/main/java/org/danekja/ymanager/business/FileService.java
index b31fa71..174a08b 100644
--- a/server/src/main/java/org/danekja/ymanager/business/FileService.java
+++ b/server/src/main/java/org/danekja/ymanager/business/FileService.java
@@ -1,6 +1,6 @@
 package org.danekja.ymanager.business;
 
-import org.danekja.ymanager.ws.rest.RESTFullException;
+import org.danekja.ymanager.ws.rest.exceptions.RESTFullException;
 
 public interface FileService {
 
diff --git a/server/src/main/java/org/danekja/ymanager/business/FileServiceImpl.java b/server/src/main/java/org/danekja/ymanager/business/FileServiceImpl.java
index 13063e9..e91954b 100644
--- a/server/src/main/java/org/danekja/ymanager/business/FileServiceImpl.java
+++ b/server/src/main/java/org/danekja/ymanager/business/FileServiceImpl.java
@@ -1,12 +1,12 @@
 package org.danekja.ymanager.business;
 
-import org.danekja.ymanager.ws.rest.RESTFullException;
 import org.apache.pdfbox.pdmodel.PDDocument;
 import org.apache.pdfbox.pdmodel.PDPage;
 import org.apache.pdfbox.pdmodel.PDPageContentStream;
 import org.apache.pdfbox.pdmodel.common.PDRectangle;
 import org.apache.pdfbox.pdmodel.font.PDType1Font;
 import org.danekja.ymanager.business.file.excel.*;
+import org.danekja.ymanager.ws.rest.exceptions.RESTFullException;
 import org.springframework.stereotype.Component;
 
 import java.io.ByteArrayInputStream;
diff --git a/server/src/main/java/org/danekja/ymanager/business/Manager.java b/server/src/main/java/org/danekja/ymanager/business/Manager.java
index 165e562..4c431a5 100644
--- a/server/src/main/java/org/danekja/ymanager/business/Manager.java
+++ b/server/src/main/java/org/danekja/ymanager/business/Manager.java
@@ -3,21 +3,18 @@ package org.danekja.ymanager.business;
 import org.danekja.ymanager.domain.RequestType;
 import org.danekja.ymanager.domain.Status;
 import org.danekja.ymanager.dto.*;
-import org.danekja.ymanager.ws.rest.RESTFullException;
+import org.danekja.ymanager.ws.rest.exceptions.RESTFullException;
 
 import java.time.LocalDate;
 import java.util.List;
 
 public interface Manager {
 
-    List<BasicProfileUser> getUsers(Status status) throws RESTFullException;
 
     List<VacationRequest> getVacationRequests(Status status) throws RESTFullException;
 
     List<AuthorizationRequest> getAuthorizationRequests(Status status) throws RESTFullException;
 
-    FullUserProfile getUserProfile(Long userId) throws RESTFullException;
-
     DefaultSettings getDefaultSettings() throws RESTFullException;
 
     List<VacationDay> getUserCalendar(Long userId, LocalDate fromDate, LocalDate toDate, Status status) throws RESTFullException;
diff --git a/server/src/main/java/org/danekja/ymanager/business/UserManager.java b/server/src/main/java/org/danekja/ymanager/business/UserManager.java
index 2ec5411..fcb298a 100644
--- a/server/src/main/java/org/danekja/ymanager/business/UserManager.java
+++ b/server/src/main/java/org/danekja/ymanager/business/UserManager.java
@@ -1,6 +1,11 @@
 package org.danekja.ymanager.business;
 
+import org.danekja.ymanager.domain.Status;
 import org.danekja.ymanager.domain.User;
+import org.danekja.ymanager.dto.BasicProfileUser;
+import org.danekja.ymanager.ws.rest.exceptions.RESTFullException;
+
+import java.util.List;
 
 /**
  * Interface for application logic handler of User entities.
@@ -8,10 +13,21 @@ import org.danekja.ymanager.domain.User;
 public interface UserManager {
 
     /**
-     * Gets user by email (username)
+     * List all users with given status.
+     * <p>
+     * TODO: refactor to return domain class instead of DTO as part of #32
+     *
+     * @param status status filter value
+     * @return list of users or empty list if none found
+     * @throws RESTFullException
+     */
+    List<BasicProfileUser> getUsers(Status status) throws RESTFullException;
+
+    /**
+     * Gets user by id (PK)
      *
-     * @param email email value, used as search key
+     * @param id id value, used as search key
      * @return found user Object or null
      */
-    User getUser(String email);
+    User getUser(Long id);
 }
diff --git a/server/src/main/java/org/danekja/ymanager/business/auth/AuthExpressions.java b/server/src/main/java/org/danekja/ymanager/business/auth/AuthExpressions.java
new file mode 100644
index 0000000..7387b24
--- /dev/null
+++ b/server/src/main/java/org/danekja/ymanager/business/auth/AuthExpressions.java
@@ -0,0 +1,21 @@
+package org.danekja.ymanager.business.auth;
+
+/**
+ * This class holds String expressions used in authorization checks.
+ * <p>
+ * Please comment each expressions well enough to clarify what constraints on user authority it enforces.
+ */
+public class AuthExpressions {
+
+    /**
+     * Used in cases where the action can be triggered by either:
+     * <ul>
+     * <li>employer - typically can edit all records</li>
+     * <li>data owner - employee can edit only his records</li>
+     * </ul>
+     * <p>
+     * In this case, the protected method needs to take <b>id</b> parameter which represents the "userId" value and
+     * is compared to principal id.
+     */
+    public static final String SELF_ONLY_ID_PARAM = "hasAuthority('EMPLOYER') or #id == authentication.principal.id";
+}
diff --git a/server/src/main/java/org/danekja/ymanager/business/auth/anot/IsEmployer.java b/server/src/main/java/org/danekja/ymanager/business/auth/anot/IsEmployer.java
new file mode 100644
index 0000000..afb4b6e
--- /dev/null
+++ b/server/src/main/java/org/danekja/ymanager/business/auth/anot/IsEmployer.java
@@ -0,0 +1,17 @@
+package org.danekja.ymanager.business.auth.anot;
+
+
+import org.springframework.security.access.prepost.PreAuthorize;
+
+import java.lang.annotation.*;
+
+/**
+ * This annotation enforces EMPLOYER user role.
+ */
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+@Documented
+@PreAuthorize("hasAuthority('EMPLOYER')")
+public @interface IsEmployer {
+}
diff --git a/server/src/main/java/org/danekja/ymanager/business/impl/DefaultUserManager.java b/server/src/main/java/org/danekja/ymanager/business/impl/DefaultUserManager.java
index 9395340..b9e1cbb 100644
--- a/server/src/main/java/org/danekja/ymanager/business/impl/DefaultUserManager.java
+++ b/server/src/main/java/org/danekja/ymanager/business/impl/DefaultUserManager.java
@@ -1,14 +1,27 @@
 package org.danekja.ymanager.business.impl;
 
 import org.danekja.ymanager.business.UserManager;
+import org.danekja.ymanager.business.auth.AuthExpressions;
+import org.danekja.ymanager.business.auth.anot.IsEmployer;
+import org.danekja.ymanager.domain.Status;
 import org.danekja.ymanager.domain.User;
+import org.danekja.ymanager.dto.BasicProfileUser;
 import org.danekja.ymanager.repository.UserRepository;
+import org.danekja.ymanager.repository.VacationRepository;
+import org.danekja.ymanager.ws.rest.exceptions.RESTFullException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.dao.DataAccessException;
+import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.security.core.userdetails.UserDetails;
 import org.springframework.security.core.userdetails.UserDetailsService;
 import org.springframework.security.core.userdetails.UsernameNotFoundException;
 import org.springframework.stereotype.Service;
 
+import java.time.LocalDate;
+import java.util.List;
+
 /**
  * Default implementation of User-related application logic.
  * <p>
@@ -17,21 +30,47 @@ import org.springframework.stereotype.Service;
 @Service
 public class DefaultUserManager implements UserManager, UserDetailsService {
 
+    private static final Logger LOG = LoggerFactory.getLogger(DefaultUserManager.class);
+
     private final UserRepository userRepository;
+    private final VacationRepository vacationRepository;
 
     @Autowired
-    public DefaultUserManager(UserRepository userRepository) {
+    public DefaultUserManager(UserRepository userRepository, VacationRepository vacationRepository) {
         this.userRepository = userRepository;
+        this.vacationRepository = vacationRepository;
+    }
+
+    @Override
+    @PreAuthorize(AuthExpressions.SELF_ONLY_ID_PARAM)
+    public User getUser(Long id) {
+        return userRepository.getUser(id);
     }
 
     @Override
-    public User getUser(String email) {
-        return userRepository.getUser(email);
+    @IsEmployer
+    public List<BasicProfileUser> getUsers(Status status) throws RESTFullException {
+        try {
+            List<BasicProfileUser> users = userRepository.getAllBasicUsers(status == null ? Status.ACCEPTED : status);
+
+            LocalDate today = LocalDate.now();
+            LocalDate weekBefore = today.minusWeeks(1);
+            LocalDate weekAfter = today.plusWeeks(1);
+            for (BasicProfileUser user : users) {
+                user.setCalendar(vacationRepository.getVacationDays(user.getId(), weekBefore, weekAfter));
+            }
+
+            return users;
+
+        } catch (DataAccessException e) {
+            LOG.error(e.getMessage());
+            throw new RESTFullException(e.getMessage(), "database.error");
+        }
     }
 
     @Override
     public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
-        UserDetails details = getUser(username);
+        UserDetails details = userRepository.getUser(username);
 
         //interface contract enforces this behavior
         if (details == null) {
diff --git a/server/src/main/java/org/danekja/ymanager/ws/rest/ApiController.java b/server/src/main/java/org/danekja/ymanager/ws/rest/ApiController.java
index d156ff4..cdc4a7d 100644
--- a/server/src/main/java/org/danekja/ymanager/ws/rest/ApiController.java
+++ b/server/src/main/java/org/danekja/ymanager/ws/rest/ApiController.java
@@ -5,7 +5,10 @@ import org.danekja.ymanager.business.FileService;
 import org.danekja.ymanager.business.Manager;
 import org.danekja.ymanager.domain.RequestType;
 import org.danekja.ymanager.domain.Status;
-import org.danekja.ymanager.dto.*;
+import org.danekja.ymanager.dto.BasicRequest;
+import org.danekja.ymanager.dto.DefaultSettings;
+import org.danekja.ymanager.dto.UserSettings;
+import org.danekja.ymanager.dto.VacationDay;
 import org.danekja.ymanager.util.localization.Language;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.HttpHeaders;
@@ -48,16 +51,6 @@ public class ApiController extends BaseController {
 
     // *********************** GET ****************************
 
-    @RequestMapping(value = "/users", method=GET)
-    public ResponseEntity users(
-            @RequestParam(value = "lang", required = false) String lang,
-            @RequestParam(value = "status", required = false) String status)
-    {
-        return handle(Language.getLanguage(lang), () ->
-                manager.getUsers(Status.getStatus(status))
-        );
-    }
-
     @RequestMapping(value = "/users/requests/vacation", method=GET)
     public ResponseEntity usersRequestsVacation(
             @RequestParam(value = "lang", required = false) String lang,
diff --git a/server/src/main/java/org/danekja/ymanager/ws/rest/BaseController.java b/server/src/main/java/org/danekja/ymanager/ws/rest/BaseController.java
index 88c6dd9..25ddf88 100644
--- a/server/src/main/java/org/danekja/ymanager/ws/rest/BaseController.java
+++ b/server/src/main/java/org/danekja/ymanager/ws/rest/BaseController.java
@@ -2,6 +2,7 @@ package org.danekja.ymanager.ws.rest;
 
 import org.danekja.ymanager.util.localization.Language;
 import org.danekja.ymanager.util.localization.Message;
+import org.danekja.ymanager.ws.rest.exceptions.RESTFullException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.http.MediaType;
diff --git a/server/src/main/java/org/danekja/ymanager/ws/rest/RESTFullException.java b/server/src/main/java/org/danekja/ymanager/ws/rest/RESTFullException.java
deleted file mode 100644
index 903cf40..0000000
--- a/server/src/main/java/org/danekja/ymanager/ws/rest/RESTFullException.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package org.danekja.ymanager.ws.rest;
-
-public class RESTFullException extends Exception {
-
-    private final String messageKey;
-
-    public RESTFullException(String message, String messageKey) {
-        super(message);
-        this.messageKey = messageKey;
-    }
-
-    @Override
-    public String getLocalizedMessage() {
-        return messageKey;
-    }
-}
diff --git a/server/src/main/java/org/danekja/ymanager/ws/rest/UserController.java b/server/src/main/java/org/danekja/ymanager/ws/rest/UserController.java
index 8cf4d84..411bab7 100644
--- a/server/src/main/java/org/danekja/ymanager/ws/rest/UserController.java
+++ b/server/src/main/java/org/danekja/ymanager/ws/rest/UserController.java
@@ -1,8 +1,11 @@
 package org.danekja.ymanager.ws.rest;
 
-import org.danekja.ymanager.business.Manager;
+import org.danekja.ymanager.business.UserManager;
+import org.danekja.ymanager.domain.Status;
 import org.danekja.ymanager.domain.User;
+import org.danekja.ymanager.dto.FullUserProfile;
 import org.danekja.ymanager.util.localization.Language;
+import org.danekja.ymanager.ws.rest.exceptions.NotFoundException;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.ResponseEntity;
 import org.springframework.security.authentication.AnonymousAuthenticationToken;
@@ -16,13 +19,23 @@ import org.springframework.web.bind.annotation.*;
 @RequestMapping("/users")
 public class UserController extends BaseController {
 
-    private final Manager manager;
+    private final UserManager userManager;
 
     @Autowired
-    public UserController(Manager manager) {
-        this.manager = manager;
+    public UserController(UserManager userManager) {
+        this.userManager = userManager;
     }
 
+    @GetMapping
+    public ResponseEntity users(
+            @RequestParam(value = "lang", required = false) String lang,
+            @RequestParam(value = "status", required = false) String status) {
+        return handle(Language.getLanguage(lang), () ->
+                userManager.getUsers(Status.getStatus(status))
+        );
+    }
+
+
     /**
      * Returns currently authenticated user.
      *
@@ -30,7 +43,7 @@ public class UserController extends BaseController {
      * @return user information object
      */
     @GetMapping("/current/profile")
-    public ResponseEntity getCurrentUser(Authentication auth) {
+    public FullUserProfile getCurrentUser(Authentication auth) throws Exception {
 
         if (auth instanceof AnonymousAuthenticationToken
                 || auth.getPrincipal() == null
@@ -38,15 +51,17 @@ public class UserController extends BaseController {
             return null;
         }
 
-        return getUserProfile(((User) auth.getPrincipal()).getId(), null);
+        return getUserProfile(((User) auth.getPrincipal()).getId());
     }
 
     @GetMapping("/{id}/profile")
-    public ResponseEntity getUserProfile(
-            @PathVariable("id") Long id,
-            @RequestParam(value = "lang", required = false) String lang) {
-        return handle(Language.getLanguage(lang), () ->
-                manager.getUserProfile(id)
-        );
+    public FullUserProfile getUserProfile(@PathVariable("id") Long id) throws Exception {
+        User u = userManager.getUser(id);
+
+        if (u == null) {
+            throw new NotFoundException();
+        }
+
+        return new FullUserProfile(u);
     }
 }
diff --git a/server/src/main/java/org/danekja/ymanager/ws/rest/exceptions/NotFoundException.java b/server/src/main/java/org/danekja/ymanager/ws/rest/exceptions/NotFoundException.java
new file mode 100644
index 0000000..0f77ad1
--- /dev/null
+++ b/server/src/main/java/org/danekja/ymanager/ws/rest/exceptions/NotFoundException.java
@@ -0,0 +1,4 @@
+package org.danekja.ymanager.ws.rest.exceptions;
+
+public class NotFoundException extends Exception {
+}
diff --git a/server/src/main/java/org/danekja/ymanager/ws/rest/exceptions/RESTFullException.java b/server/src/main/java/org/danekja/ymanager/ws/rest/exceptions/RESTFullException.java
new file mode 100644
index 0000000..fd48d73
--- /dev/null
+++ b/server/src/main/java/org/danekja/ymanager/ws/rest/exceptions/RESTFullException.java
@@ -0,0 +1,16 @@
+package org.danekja.ymanager.ws.rest.exceptions;
+
+public class RESTFullException extends Exception {
+
+    private final String messageKey;
+
+    public RESTFullException(String message, String messageKey) {
+        super(message);
+        this.messageKey = messageKey;
+    }
+
+    @Override
+    public String getLocalizedMessage() {
+        return messageKey;
+    }
+}
diff --git a/webapp/src/app/services/api/user.service.ts b/webapp/src/app/services/api/user.service.ts
index 5a3ad6d..676240f 100644
--- a/webapp/src/app/services/api/user.service.ts
+++ b/webapp/src/app/services/api/user.service.ts
@@ -3,21 +3,22 @@ import {HttpClient, HttpParams} from '@angular/common/http';
 
 import {Calendar, CalendarEdit, PostCalendar} from '../../models/calendar.model';
 import {BasicService} from './basic.service';
-import {catchError} from 'rxjs/operators';
+import {catchError, flatMap} from 'rxjs/operators';
 import {Languages, RequestStatus, RequestTypes} from '../../enums/common.enum';
 import {NotificationSettings, UserSettings} from '../../models/settings.model';
 import {UserRequest} from '../../models/requests.model';
 import {MatSnackBar} from '@angular/material';
 import {DateFormatterService} from '../util/date-formatter.service';
 import {TranslateService} from '@ngx-translate/core';
+import {ProfileService} from "../util/profile.service";
 
 @Injectable({
   providedIn: 'root'
 })
 export class UserService extends BasicService { // dost podobny k usersService, mozna zmenit v rest api
-  private _userUrl = this.baseUrl + '/api/users/';
+  private _userUrl = this.baseUrl + '/api/user/';
 
-  constructor(protected http: HttpClient, protected snackBar: MatSnackBar, protected translateService: TranslateService, private dateFormater: DateFormatterService) {
+  constructor(protected http: HttpClient, protected snackBar: MatSnackBar, protected translateService: TranslateService, protected profileService: ProfileService, private dateFormater: DateFormatterService) {
     super(http, snackBar, translateService);
   }
 
@@ -27,7 +28,7 @@ export class UserService extends BasicService { // dost podobny k usersService,
    * @param from returns days from this date forward
    */
   getLoggedUserCalendar(from: Date) {
-    return this.makeGetCalendarApiCall('me', from, null, null, null);
+    return this.profileService.getLoggedUser(true).pipe(flatMap((profile) => this.makeGetCalendarApiCall(profile.id.toString(), from, null, null, null)));
   }
 
   /**
@@ -39,7 +40,7 @@ export class UserService extends BasicService { // dost podobny k usersService,
    * @param status filter by status
    */
   getLoggedUserCalendarWithOptions(from: Date, to: Date, language: Languages, status: RequestStatus) {
-    return this.makeGetCalendarApiCall('me', from, to, language, status);
+    return this.profileService.getLoggedUser(true).pipe(flatMap((profile) => this.makeGetCalendarApiCall(profile.id.toString(), from, to, language, status)));
   }
 
   /**
diff --git a/webapp/src/app/services/api/users.service.ts b/webapp/src/app/services/api/users.service.ts
index b44798a..cb72294 100644
--- a/webapp/src/app/services/api/users.service.ts
+++ b/webapp/src/app/services/api/users.service.ts
@@ -201,8 +201,8 @@ export class UsersService extends BasicService {
 
   /**
    * Získání profilu aktuálně přihlášeného uživatele nebo uživatele podle zadaného id.
-   * GET /user/<{id} || me>/profile?[lang=<CZ,EN>]
-   * @param id id of profile to get (number or 'me')
+   * GET /user/<{id} || current>/profile?[lang=<CZ,EN>]
+   * @param id id of profile to get (number or 'current')
    * @param language filter by language
    */
   private makeGetProfileApiCall(id: string, language: string) {
