Commit bc79d18c authored by Sonia Zorba's avatar Sonia Zorba
Browse files

Implemented keep alive and audit logging; minor improvements

parent d6ee290f
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -8,29 +8,29 @@
"lint": "vue-cli-service lint"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.19",
"@fortawesome/free-solid-svg-icons": "^5.9.0",
"@fortawesome/vue-fontawesome": "^0.1.6",
"bootstrap-vue": "^2.0.0-rc.27",
"core-js": "^2.6.5",
"@fortawesome/fontawesome-svg-core": "^1.2.25",
"@fortawesome/free-solid-svg-icons": "^5.11.2",
"@fortawesome/vue-fontawesome": "^0.1.8",
"bootstrap-vue": "^2.1.0",
"core-js": "^2.6.10",
"debounce": "^1.2.0",
"vue": "^2.6.10",
"vuex": "^3.1.1"
"vuex": "^3.1.2"
},
"devDependencies": {
"@babel/polyfill": "^7.4.4",
"@vue/cli-plugin-babel": "^3.8.0",
"@vue/cli-plugin-eslint": "^3.8.0",
"@vue/cli-service": "^3.8.0",
"babel-eslint": "^10.0.1",
"bootstrap": "^4.3.1",
"@babel/polyfill": "^7.7.0",
"@vue/cli-plugin-babel": "^3.12.1",
"@vue/cli-plugin-eslint": "^3.12.1",
"@vue/cli-service": "^3.12.1",
"babel-eslint": "^10.0.3",
"bootstrap": "^4.4.1",
"eslint": "^5.16.0",
"eslint-plugin-vue": "^5.0.0",
"mutationobserver-shim": "^0.3.3",
"node-sass": "^4.12.0",
"popper.js": "^1.15.0",
"portal-vue": "^2.1.4",
"sass-loader": "^7.1.0",
"node-sass": "^4.13.0",
"popper.js": "^1.16.0",
"portal-vue": "^2.1.6",
"sass-loader": "^7.3.1",
"vue-cli-plugin-bootstrap-vue": "^0.4.0",
"vue-template-compiler": "^2.6.10"
},
......
......@@ -56,6 +56,8 @@ export default {
.then(model => {
this.$store.commit('updateHomePageModel', model);
});
setInterval(client.keepAlive, 60000);
}
}
</script>
......
......@@ -8,7 +8,7 @@ import permission from './data/permission';
import search from './data/search';
import openUserSearchResult from './data/openUserSearchResult';
const fetch = (mockData, time = 0) => {
const fetch = (mockData, time = 500) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(mockData)
......@@ -18,51 +18,54 @@ const fetch = (mockData, time = 0) => {
export default {
fetchHomePageModel() {
return fetch(home, 500);
return fetch(home);
},
fetchGroupsTab() {
return fetch(groups, 500);
return fetch(groups);
},
fetchGroupsPanel() {
return fetch(groupsPanel, 500);
return fetch(groupsPanel);
},
fetchMembersPanel() {
return fetch(membersPanel, 500);
return fetch(membersPanel);
},
fetchPermissionsPanel() {
return fetch(permissionsPanel, 500);
return fetch(permissionsPanel);
},
addGroup() {
return fetch(groupsPanel, 500);
return fetch(groupsPanel);
},
renameGroup() {
return fetch(groupsPanel, 500);
return fetch(groupsPanel);
},
removeGroup() {
return fetch(groupsPanel, 500);
return fetch(groupsPanel);
},
searchUser() {
return fetch(searchUser, 500);
return fetch(searchUser);
},
addPermission() {
return fetch(permissionsPanel, 500);
return fetch(permissionsPanel);
},
getPermission() {
return fetch(permission, 500);
return fetch(permission);
},
removePermission() {
return fetch(permissionsPanel, 500);
return fetch(permissionsPanel);
},
addMember() {
return fetch(membersPanel, 500);
return fetch(membersPanel);
},
removeMember() {
return fetch(membersPanel, 500);
return fetch(membersPanel);
},
search() {
return fetch(search, 500);
return fetch(search);
},
openUserSearchResult() {
return fetch(openUserSearchResult, 500);
return fetch(openUserSearchResult);
},
setKeepAlive() {
return fetch({});
}
}
const BASE_API_URL = process.env.VUE_APP_API_BASE_URL;
function apiRequest(url, options) {
loading(true);
function apiRequest(url, options, showLoading = true) {
if (showLoading) {
loading(true);
}
return new Promise((resolve) => {
fetch(url, options)
.then(response => {
loading(false);
if ([200, 201, 204, 400].includes(response.status)) { // valid status codes
resolve(response.json());
if (response.status === 204) {
resolve({});
} else {
resolve(response.json());
}
} else {
response.json().then(jsonValue => dispatchApiErrorEvent(jsonValue));
}
......@@ -304,5 +310,14 @@ export default {
'Accept': 'application/json',
}
});
},
keepAlive() {
let url = BASE_API_URL + 'keepAlive';
return apiRequest(url, {
method: 'GET',
cache: 'no-cache',
credentials: 'include'
}, false);
}
};
......@@ -4,7 +4,7 @@
<label class="w-25" for="new-group-name-input">Group name:</label>
<b-form-input v-model="newGroupName" id="new-group-name-input" ref="newGroupNameInput" class="w-75" aria-describedby="new-group-name-input-feedback" :state="newGroupNameState" v-on:input="resetError" @keydown.native.enter="addGroup">
</b-form-input>
<b-form-invalid-feedback id="new-group-name-input-feedback">{{newGroupNameError}}</b-form-invalid-feedback>
<b-form-invalid-feedback id="new-group-name-input-feedback" class="text-right">{{newGroupNameError}}</b-form-invalid-feedback>
<b-form-checkbox class="mt-3 ml-3" v-model="leaf">is leaf</b-form-checkbox>
</b-form>
</b-modal>
......@@ -18,14 +18,15 @@ export default {
computed: {
newGroupNameState() {
if (this.newGroupNameError) {
return 'invalid';
return false;
}
return null;
}
},
data: function() {
return {
newGroupName: null,
newGroupNameError: null,
newGroupName: '',
newGroupNameError: '',
leaf: false
};
},
......
......@@ -4,7 +4,7 @@
<label class="w-25" for="new-group-name-input">Group name:</label>
<b-form-input v-model="newGroupName" id="new-group-name-input" class="w-75" aria-describedby="new-group-name-input-feedback" :state="newGroupNameState" v-on:input="resetError">
</b-form-input>
<b-form-invalid-feedback id="new-group-name-input-feedback">{{newGroupNameError}}</b-form-invalid-feedback>
<b-form-invalid-feedback id="new-group-name-input-feedback" class="text-right">{{newGroupNameError}}</b-form-invalid-feedback>
<b-form-checkbox class="mt-3 ml-3" v-model="leaf">is leaf</b-form-checkbox>
</b-form>
</b-modal>
......@@ -18,16 +18,17 @@ export default {
computed: {
newGroupNameState() {
if (this.newGroupNameError) {
return 'invalid';
return false;
}
return null;
}
},
data: function() {
return {
groupId: null,
oldGroupName: null,
newGroupName: null,
newGroupNameError: null,
groupId: '',
oldGroupName: '',
newGroupName: '',
newGroupNameError: '',
leaf: false
};
},
......@@ -55,7 +56,6 @@ export default {
return;
}
let parent = this.$store.getters.selectedGroupId;
client.renameGroup(this.groupId, this.newGroupName, this.leaf, this.$store.state.input)
.then(res => {
if (res.status === 400) {
......
package it.inaf.ia2.gms.authn;
import it.inaf.ia2.gms.persistence.LoggingDAO;
import java.util.List;
import java.util.Map;
import org.springframework.security.core.Authentication;
......@@ -12,9 +13,11 @@ import org.springframework.security.oauth2.provider.token.store.jwk.JwkTokenStor
public class CustomIdTokenConverter extends DefaultUserAuthenticationConverter {
private final JwkTokenStore jwkTokenStore;
private final LoggingDAO loggingDAO;
public CustomIdTokenConverter(JwkTokenStore jwkTokenStore) {
public CustomIdTokenConverter(JwkTokenStore jwkTokenStore, LoggingDAO loggingDAO) {
this.jwkTokenStore = jwkTokenStore;
this.loggingDAO = loggingDAO;
}
@Override
......@@ -29,6 +32,7 @@ public class CustomIdTokenConverter extends DefaultUserAuthenticationConverter {
Map<String, Object> claims = token.getAdditionalInformation();
String principal = (String) claims.get("sub");
loggingDAO.logAction("Login by " + principal);
List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("ROLE_USER");
......
package it.inaf.ia2.gms.authn;
import it.inaf.ia2.gms.persistence.LoggingDAO;
import java.io.IOException;
import java.security.Principal;
import java.util.Map;
......@@ -17,9 +18,11 @@ import org.springframework.security.oauth2.provider.token.store.jwk.JwkTokenStor
public class JWTFilter implements Filter {
private final JwkTokenStore jwkTokenStore;
private final LoggingDAO loggingDAO;
public JWTFilter(JwkTokenStore jwkTokenStore) {
public JWTFilter(JwkTokenStore jwkTokenStore, LoggingDAO loggingDAO) {
this.jwkTokenStore = jwkTokenStore;
this.loggingDAO = loggingDAO;
}
@Override
......@@ -30,6 +33,7 @@ public class JWTFilter implements Filter {
String authHeader = request.getHeader("Authorization");
if (authHeader == null) {
loggingDAO.logAction("Attempt to access WS without token");
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Missing Authorization token");
return;
}
......@@ -38,6 +42,7 @@ public class JWTFilter implements Filter {
OAuth2AccessToken accessToken = jwkTokenStore.readAccessToken(authHeader);
if (accessToken.isExpired()) {
loggingDAO.logAction("Attempt to access WS with expired token");
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Access token is expired");
return;
}
......@@ -45,11 +50,13 @@ public class JWTFilter implements Filter {
Map<String, Object> claims = accessToken.getAdditionalInformation();
if (claims.get("sub") == null) {
loggingDAO.logAction("Attempt to access WS with invalid token");
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid access token: missing sub claim");
return;
}
ServletRequest wrappedRequest = new ServletRequestWithJWTPrincipal(request, claims);
ServletRequestWithJWTPrincipal wrappedRequest = new ServletRequestWithJWTPrincipal(request, claims);
loggingDAO.logAction("WS access from " + wrappedRequest.getUserPrincipal().getName());
fc.doFilter(wrappedRequest, res);
}
......
package it.inaf.ia2.gms.authn;
import it.inaf.ia2.gms.persistence.LoggingDAO;
import java.util.List;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Value;
......@@ -32,11 +33,11 @@ public class OAuth2Config extends AuthorizationServerEndpointsConfiguration {
private String clientId;
@Bean
public ResourceServerTokenServices resourceServerTokenServices(JwkTokenStore jwkTokenStore) {
public ResourceServerTokenServices resourceServerTokenServices(JwkTokenStore jwkTokenStore, LoggingDAO loggingDAO) {
GetTokenDataService tokenService = new GetTokenDataService();
DefaultAccessTokenConverter accessTokenConverter = new DefaultAccessTokenConverter();
accessTokenConverter.setUserTokenConverter(new CustomIdTokenConverter(jwkTokenStore));
accessTokenConverter.setUserTokenConverter(new CustomIdTokenConverter(jwkTokenStore, loggingDAO));
tokenService.setAccessTokenConverter(accessTokenConverter);
tokenService.setCheckTokenEndpointUrl(checkTokenEndpointUrl);
......
package it.inaf.ia2.gms.authn;
import it.inaf.ia2.gms.persistence.LoggingDAO;
import java.util.Arrays;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
......@@ -70,9 +71,9 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
* Checks the BasicAuth for GMS clients.
*/
@Bean
public FilterRegistrationBean serviceBasicAuthFilter() {
public FilterRegistrationBean serviceBasicAuthFilter(LoggingDAO loggingDAO) {
FilterRegistrationBean bean = new FilterRegistrationBean();
bean.setFilter(new ServiceBasicAuthFilter());
bean.setFilter(new ServiceBasicAuthFilter(loggingDAO));
bean.addUrlPatterns("/ws/basic/*");
bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return bean;
......@@ -82,9 +83,9 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
* Checks JWT for web services.
*/
@Bean
public FilterRegistrationBean serviceJWTFilter(JwkTokenStore jwkTokenStore) {
public FilterRegistrationBean serviceJWTFilter(JwkTokenStore jwkTokenStore, LoggingDAO loggingDAO) {
FilterRegistrationBean bean = new FilterRegistrationBean();
bean.setFilter(new JWTFilter(jwkTokenStore));
bean.setFilter(new JWTFilter(jwkTokenStore, loggingDAO));
bean.addUrlPatterns("/ws/jwt/*");
bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return bean;
......
......@@ -2,6 +2,7 @@ package it.inaf.ia2.gms.authn;
import it.inaf.ia2.gms.exception.UnauthorizedException;
import it.inaf.ia2.gms.persistence.ClientsDAO;
import it.inaf.ia2.gms.persistence.LoggingDAO;
import it.inaf.ia2.gms.persistence.model.ClientEntity;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
......@@ -22,6 +23,12 @@ import org.springframework.web.context.support.WebApplicationContextUtils;
public class ServiceBasicAuthFilter implements Filter {
private final LoggingDAO loggingDAO;
public ServiceBasicAuthFilter(LoggingDAO loggingDAO) {
this.loggingDAO = loggingDAO;
}
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
......@@ -31,11 +38,14 @@ public class ServiceBasicAuthFilter implements Filter {
try {
validateBasicAuth(request);
} catch (UnauthorizedException ex) {
loggingDAO.logAction("Unauthorized BasicAuth WS request");
((HttpServletResponse) res).sendError(HttpServletResponse.SC_UNAUTHORIZED, ex.getMessage());
return;
}
}
loggingDAO.logAction("BasicAuth WS request");
chain.doFilter(req, res);
}
......
......@@ -18,6 +18,7 @@ public class SessionData {
private String userName;
private String accessToken;
private String refreshToken;
private long expiration;
@PostConstruct
public void init() {
......@@ -26,6 +27,7 @@ public class SessionData {
userName = (String) authn.getAttributes().get("name");
accessToken = (String) authn.getAccessToken().getValue();
refreshToken = authn.getRefreshToken();
setExpiresIn(authn.getAccessToken().getExpiresIn());
}
public String getUserId() {
......@@ -51,4 +53,12 @@ public class SessionData {
public String getUserName() {
return userName;
}
public void setExpiresIn(int expiresIn) {
this.expiration = System.currentTimeMillis() + expiresIn * 1000;
}
public long getExpiresIn() {
return (System.currentTimeMillis() - expiration) / 1000;
}
}
......@@ -11,6 +11,7 @@ import it.inaf.ia2.gms.model.request.DeleteGroupRequest;
import it.inaf.ia2.gms.model.request.GroupsRequest;
import it.inaf.ia2.gms.model.request.RenameGroupRequest;
import it.inaf.ia2.gms.model.request.SearchFilterRequest;
import it.inaf.ia2.gms.persistence.LoggingDAO;
import it.inaf.ia2.gms.persistence.model.GroupEntity;
import it.inaf.ia2.gms.service.GroupsService;
import it.inaf.ia2.gms.service.GroupsTreeBuilder;
......@@ -46,6 +47,9 @@ public class GroupsController {
@Autowired
private GroupsTabResponseBuilder groupsTabResponseBuilder;
@Autowired
private LoggingDAO loggingDAO;
@GetMapping(value = "/groups", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public ResponseEntity<?> getGroupsTab(@Valid GroupsRequest request) {
if (request.isOnlyPanel()) {
......@@ -64,10 +68,12 @@ public class GroupsController {
GroupEntity parent = groupsService.getGroupById(request.getParentGroupId());
if (permissionsService.getUserPermissionForGroup(parent, session.getUserId()) != Permission.ADMIN) {
loggingDAO.logAction("Unauthorized create group request, group_name=" + request.getNewGroupName());
throw new UnauthorizedException("Missing admin permission");
}
groupsService.addGroup(parent, request.getNewGroupName(), request.isLeaf());
loggingDAO.logAction("Added group: parent_path=" + parent.getPath() + ", group_name=" + request.getNewGroupName());
PaginatedData<GroupNode> groupsPanel = getGroupsPanel(parent, request);
......@@ -80,10 +86,12 @@ public class GroupsController {
GroupEntity group = groupsService.getGroupById(groupId);
if (permissionsService.getUserPermissionForGroup(group, session.getUserId()) != Permission.ADMIN) {
loggingDAO.logAction("Unauthorized rename group request, group_id=" + groupId);
throw new UnauthorizedException("Missing admin permission");
}
GroupEntity renamedGroup = groupsService.renameGroup(group, request.getNewGroupName(), request.isLeaf());
loggingDAO.logAction("Group renamed, group_id=" + groupId + ", new name: " + request.getNewGroupName());
GroupEntity parent = groupsService.getGroupByPath(renamedGroup.getParentPath());
......@@ -98,11 +106,12 @@ public class GroupsController {
GroupEntity group = groupsService.getGroupById(groupId);
if (permissionsService.getUserPermissionForGroup(group, session.getUserId()) != Permission.ADMIN) {
loggingDAO.logAction("Unauthorized delete group request, group_id=" + groupId);
throw new UnauthorizedException("Missing admin permission");
}
GroupEntity parent = groupsService.deleteGroup(group);
loggingDAO.logAction("Group deleted, group_id=" + groupId);
PaginatedData<GroupNode> groupsPanel = getGroupsPanel(parent, request);
return ResponseEntity.ok(groupsPanel);
......
package it.inaf.ia2.gms.controller;
import it.inaf.ia2.gms.authn.SessionData;
import it.inaf.ia2.gms.rap.RapClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class KeepAliveController {
private static final Logger LOG = LoggerFactory.getLogger(KeepAliveController.class);
@Autowired
private SessionData sessionData;
@Autowired
private RapClient rapClient;
@GetMapping("/keepAlive")
public ResponseEntity<?> keepAlive() {
LOG.trace("Keepalive called");
if (sessionData.getExpiresIn() < 60) {
rapClient.refreshToken();
LOG.trace("RAP token refreshed");
}
return ResponseEntity.noContent().build();
}
}
......@@ -9,6 +9,7 @@ import it.inaf.ia2.gms.model.Permission;
import it.inaf.ia2.gms.model.RapUser;
import it.inaf.ia2.gms.model.request.RemoveMemberRequest;
import it.inaf.ia2.gms.model.request.TabRequest;
import it.inaf.ia2.gms.persistence.LoggingDAO;
import it.inaf.ia2.gms.persistence.model.GroupEntity;
import it.inaf.ia2.gms.service.GroupsService;
import it.inaf.ia2.gms.service.MembersService;
......@@ -40,6 +41,9 @@ public class MembersController {
@Autowired
private MembersService membersService;
@Autowired
private LoggingDAO loggingDAO;
@GetMapping(value = "/members", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public ResponseEntity<PaginatedData<RapUser>> getMembersTab(TabRequest request) {
......@@ -66,9 +70,13 @@ public class MembersController {
if (currentUserPermission == Permission.MANAGE_MEMBERS) {
// Automatically assign the VIEW_MEMBERS permission ("Add collaborator" feature)
permissionsService.addPermission(group, request.getUserId(), Permission.VIEW_MEMBERS);
loggingDAO.logAction("Added permission, group_id=" + group.getId() + ", user_id="
+ request.getUserId() + ", permission=" + Permission.VIEW_MEMBERS);
} else if (request.getPermission() != null) {
// Admin users can specify a permission
permissionsService.addPermission(group, request.getUserId(), request.getPermission());
loggingDAO.logAction("Added permission, group_id=" + group.getId() + ", user_id="
+ request.getUserId() + ", permission=" + request.getPermission());
}
return new ResponseEntity<>(getMembersPanel(request), HttpStatus.CREATED);
......@@ -81,6 +89,7 @@ public class MembersController {
Permission currentUserPermission = verifyCurrentUserCanManageMembers(group);
membersService.removeMember(group.getId(), request.getUserId());
loggingDAO.logAction("Member removed, group_id=" + group.getId() + ", user_id=" + request.getUserId());
// For users having the MANAGE_MEMBERS permission, the VIEW_MEMBERS permission
// is automatically assigned when they add a member ("Add collaborator" feature).
......@@ -95,6 +104,7 @@ public class MembersController {
if (removeCollaborator || adminRemovePermission) {
permissionsService.removePermission(group, request.getUserId());
loggingDAO.logAction("Permission removed, group_id=" + group.getId() + ", user_id=" + request.getUserId());
}
return ResponseEntity.ok(getMembersPanel(request));
......
......@@ -9,6 +9,7 @@ import it.inaf.ia2.gms.model.request.PaginatedModelRequest;
import it.inaf.ia2.gms.model.Permission;
import it.inaf.ia2.gms.model.UserPermission;
import it.inaf.ia2.gms.model.request.TabRequest;
import it.inaf.ia2.gms.persistence.LoggingDAO;
import it.inaf.ia2.gms.persistence.model.GroupEntity;
import it.inaf.ia2.gms.service.GroupsService;
import it.inaf.ia2.gms.service.PermissionsService;
......@@ -39,6 +40,9 @@ public class PermissionsController {
@Autowired
private PermissionsService permissionsService;
@Autowired
private LoggingDAO loggingDAO;
@GetMapping(value = "/permissions", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)