Merge branch 'master' into develop

# Conflicts:
#	backend/pom.xml
#	distribution/pom.xml
#	frontend/pom.xml
#	pom.xml
#	resources/pom.xml
This commit is contained in:
Zaripov Emil 2024-11-20 11:16:55 +03:00
commit 67214fccdc
11 changed files with 194 additions and 57 deletions

View file

@ -1,10 +1,13 @@
package ru.micord.ervu.kafka.controller; package ru.micord.ervu.kafka.controller;
import java.lang.invoke.MethodHandles;
import javax.servlet.http.Cookie; import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import ervu.client.fileupload.WebDavClient; import ervu.client.fileupload.WebDavClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
@ -22,6 +25,7 @@ import ru.micord.ervu.security.webbpm.jwt.service.JwtTokenService;
*/ */
@RestController @RestController
public class ErvuKafkaController { public class ErvuKafkaController {
private static final Logger LOGGER = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
@Autowired @Autowired
private ReplyingKafkaService replyingKafkaService; private ReplyingKafkaService replyingKafkaService;
@ -56,7 +60,14 @@ public class ErvuKafkaController {
objectMapper.writeValueAsString(data) objectMapper.writeValueAsString(data)
); );
ExcerptResponse excerptResponse = objectMapper.readValue(kafkaResponse, ExcerptResponse.class); ExcerptResponse excerptResponse = objectMapper.readValue(kafkaResponse, ExcerptResponse.class);
return webDavClient.webDavDownloadFile(excerptResponse.getFileUrl()); if (!excerptResponse.getSuccess()) {
throw new RuntimeException("Error with getting excerpt url " + excerptResponse.getMessage());
}
else if (excerptResponse.getData() == null || excerptResponse.getData().getFileUrl() == null
|| excerptResponse.getData().getFileUrl().isEmpty()) {
return ResponseEntity.noContent().build();
}
return webDavClient.webDavDownloadFile(excerptResponse.getData().getFileUrl());
} }
catch (Exception e) { catch (Exception e) {
throw new RuntimeException(e); throw new RuntimeException(e);

View file

@ -0,0 +1,31 @@
package ru.micord.ervu.kafka.model;
import java.io.Serializable;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
/**
* @author Eduard Tihomirov
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public class ExcerptData implements Serializable {
private static final long serialVersionUID = 1L;
private String orgId;
private String fileUrl;
public String getOrgId() {
return orgId;
}
public void setOrgId(String orgId) {
this.orgId = orgId;
}
public String getFileUrl() {
return fileUrl;
}
public void setFileUrl(String fileUrl) {
this.fileUrl = fileUrl;
}
}

View file

@ -9,24 +9,31 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
public class ExcerptResponse implements Serializable { public class ExcerptResponse implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private boolean success;
private String message;
private ExcerptData data;
private String orgId; public boolean getSuccess() {
return success;
private String fileUrl;
public String getOrgId() {
return orgId;
} }
public void setOrgId(String orgId) { public void setSuccess(boolean success) {
this.orgId = orgId; this.success = success;
} }
public String getFileUrl() { public String getMessage() {
return fileUrl; return message;
} }
public void setFileUrl(String fileUrl) { public void setMessage(String message) {
this.fileUrl = fileUrl; this.message = message;
}
public ExcerptData getData() {
return data;
}
public void setData(ExcerptData data) {
this.data = data;
} }
} }

View file

@ -13,6 +13,7 @@ import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutFilter; import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository; import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfTokenRequestHandler; import org.springframework.security.web.csrf.CsrfTokenRequestHandler;
import org.springframework.security.web.csrf.XorCsrfTokenRequestAttributeHandler; import org.springframework.security.web.csrf.XorCsrfTokenRequestAttributeHandler;
@ -49,31 +50,22 @@ public class SecurityConfig {
} }
@Bean @Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { public SecurityFilterChain filterChain(HttpSecurity http,
httpConfigure(http); CookieCsrfTokenRepository tokenRepository)
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); throws Exception {
http.addFilterBefore(new RequestContextFilter(), LogoutFilter.class);
http.addFilterAfter(filterChainExceptionHandler, RequestContextFilter.class);
return http.build();
}
protected void httpConfigure(HttpSecurity httpSecurity) throws Exception {
CookieCsrfTokenRepository tokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse();
tokenRepository.setCookieName(TokenConstants.CSRF_TOKEN_NAME);
tokenRepository.setHeaderName(TokenConstants.CSRF_HEADER_NAME);
tokenRepository.setCookiePath("/");
XorCsrfTokenRequestAttributeHandler delegate = new XorCsrfTokenRequestAttributeHandler(); XorCsrfTokenRequestAttributeHandler delegate = new XorCsrfTokenRequestAttributeHandler();
delegate.setCsrfRequestAttributeName(null); delegate.setCsrfRequestAttributeName(null);
// Use only the handle() method of XorCsrfTokenRequestAttributeHandler and the // Use only the handle() method of XorCsrfTokenRequestAttributeHandler and the
// default implementation of resolveCsrfTokenValue() from CsrfTokenRequestHandler // default implementation of resolveCsrfTokenValue() from CsrfTokenRequestHandler
CsrfTokenRequestHandler requestHandler = delegate::handle; CsrfTokenRequestHandler requestHandler = delegate::handle;
httpSecurity.authorizeHttpRequests( http.authorizeHttpRequests(
(authorizeHttpRequests) -> authorizeHttpRequests.requestMatchers(PERMIT_ALL) (authorizeHttpRequests) -> authorizeHttpRequests.requestMatchers(PERMIT_ALL)
.permitAll() .permitAll()
.anyRequest() .anyRequest()
.authenticated()) .authenticated())
.csrf((csrf) -> csrf.csrfTokenRepository(tokenRepository) .csrf((csrf) -> csrf.csrfTokenRepository(tokenRepository)
.csrfTokenRequestHandler(requestHandler)) .csrfTokenRequestHandler(requestHandler)
.sessionAuthenticationStrategy(new NullAuthenticatedSessionStrategy()))
.logout((logout) -> logout.logoutUrl(ESIA_LOGOUT) .logout((logout) -> logout.logoutUrl(ESIA_LOGOUT)
.logoutSuccessHandler(new LogoutSuccessHandler(tokenRepository, esiaAuthService))) .logoutSuccessHandler(new LogoutSuccessHandler(tokenRepository, esiaAuthService)))
.exceptionHandling() .exceptionHandling()
@ -81,6 +73,19 @@ public class SecurityConfig {
.and() .and()
.sessionManagement() .sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS); .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
http.addFilterBefore(new RequestContextFilter(), LogoutFilter.class);
http.addFilterAfter(filterChainExceptionHandler, RequestContextFilter.class);
return http.build();
}
@Bean
public CookieCsrfTokenRepository cookieCsrfTokenRepository() {
CookieCsrfTokenRepository tokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse();
tokenRepository.setCookieName(TokenConstants.CSRF_TOKEN_NAME);
tokenRepository.setHeaderName(TokenConstants.CSRF_HEADER_NAME);
tokenRepository.setCookiePath("/");
return tokenRepository;
} }
public AuthenticationEntryPoint entryPoint() { public AuthenticationEntryPoint entryPoint() {

View file

@ -22,6 +22,7 @@ import javax.servlet.http.HttpServletResponse;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import ervu.service.okopf.OkopfService; import ervu.service.okopf.OkopfService;
import org.springframework.security.authentication.AuthenticationManager;
import ru.micord.ervu.security.esia.token.TokensStore; import ru.micord.ervu.security.esia.token.TokensStore;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -50,7 +51,6 @@ import ru.micord.ervu.security.webbpm.jwt.JwtAuthentication;
import ru.micord.ervu.security.webbpm.jwt.helper.SecurityHelper; import ru.micord.ervu.security.webbpm.jwt.helper.SecurityHelper;
import ru.micord.ervu.security.webbpm.jwt.service.JwtTokenService; import ru.micord.ervu.security.webbpm.jwt.service.JwtTokenService;
import ru.micord.ervu.security.webbpm.jwt.model.Token; import ru.micord.ervu.security.webbpm.jwt.model.Token;
import ru.micord.ervu.security.webbpm.jwt.util.SecurityUtil;
/** /**
* @author Eduard Tihomirov * @author Eduard Tihomirov
@ -72,6 +72,8 @@ public class EsiaAuthService {
private OkopfService okopfService; private OkopfService okopfService;
@Autowired @Autowired
private SecurityHelper securityHelper; private SecurityHelper securityHelper;
@Autowired
private AuthenticationManager authenticationManager;
@Value("${ervu.kafka.org.reply.topic}") @Value("${ervu.kafka.org.reply.topic}")
private String requestReplyTopic; private String requestReplyTopic;
@ -211,16 +213,16 @@ public class EsiaAuthService {
if (tokenResponse.getError() != null) { if (tokenResponse.getError() != null) {
throw new RuntimeException(tokenResponse.getError_description()); throw new RuntimeException(tokenResponse.getError_description());
} }
String accessToken = tokenResponse.getAccess_token(); String esiaAccessTokenStr = tokenResponse.getAccess_token();
String esiaRefreshTokenStr = tokenResponse.getRefresh_token();
boolean hasRole = ulDataService.checkRole(accessToken); boolean hasRole = ulDataService.checkRole(esiaAccessTokenStr);
EsiaAccessToken esiaAccessToken = ulDataService.readToken(accessToken); EsiaAccessToken esiaAccessToken = ulDataService.readToken(esiaAccessTokenStr);
String prnOid = esiaAccessToken.getSbj_id(); String prnOid = esiaAccessToken.getSbj_id();
String refreshToken = tokenResponse.getRefresh_token(); String ervuId = getErvuId(esiaAccessTokenStr, prnOid);
String ervuId = getErvuId(accessToken, prnOid);
Long expiresIn = tokenResponse.getExpires_in(); Long expiresIn = tokenResponse.getExpires_in();
TokensStore.addAccessToken(prnOid, accessToken, expiresIn); TokensStore.addAccessToken(prnOid, esiaAccessTokenStr, expiresIn);
TokensStore.addRefreshToken(prnOid, refreshToken, expiresIn); TokensStore.addRefreshToken(prnOid, esiaRefreshTokenStr, expiresIn);
Token token = jwtTokenService.createAccessToken(esiaAccessToken.getSbj_id(), expiresIn, ervuId, hasRole); Token token = jwtTokenService.createAccessToken(esiaAccessToken.getSbj_id(), expiresIn, ervuId, hasRole);
int expiry = tokenResponse.getExpires_in().intValue(); int expiry = tokenResponse.getExpires_in().intValue();
Cookie accessCookie = securityHelper.createAccessCookie(token.getValue(), expiry); Cookie accessCookie = securityHelper.createAccessCookie(token.getValue(), expiry);
@ -228,9 +230,10 @@ public class EsiaAuthService {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(token.getUserAccountId(), null); new UsernamePasswordAuthenticationToken(token.getUserAccountId(), null);
SecurityContext context = SecurityContextHolder.createEmptyContext(); SecurityContext context = SecurityContextHolder.createEmptyContext();
JwtAuthentication authentication = new JwtAuthentication(usernamePasswordAuthenticationToken, JwtAuthentication jwtAuthentication = new JwtAuthentication(usernamePasswordAuthenticationToken,
esiaAccessToken.getSbj_id(), token.getValue()); esiaAccessToken.getSbj_id(), token.getValue());
context.setAuthentication(authentication); authenticationManager.authenticate(jwtAuthentication);
context.setAuthentication(jwtAuthentication);
SecurityContextHolder.setContext(context); SecurityContextHolder.setContext(context);
Cookie authMarkerCookie = securityHelper.createAuthMarkerCookie("true", expiry); Cookie authMarkerCookie = securityHelper.createAuthMarkerCookie("true", expiry);
response.addCookie(authMarkerCookie); response.addCookie(authMarkerCookie);

View file

@ -245,8 +245,10 @@ public class UlDataServiceImpl implements UlDataService {
JsonNode elementsNode = rootNode.path("elements"); JsonNode elementsNode = rootNode.path("elements");
StringBuilder names = new StringBuilder(); StringBuilder names = new StringBuilder();
for (JsonNode element : elementsNode) { for (JsonNode element : elementsNode) {
String name = element.path("name").asText(); if (element.path("itSystem").asText().equals(esiaConfig.getClientId())) {
names.append(name).append("\n"); String name = element.path("name").asText();
names.append(name).append("\n");
}
} }
return names.toString(); return names.toString();
} }

View file

@ -0,0 +1,72 @@
package ru.micord.ervu.security.listener;
import java.lang.invoke.MethodHandles;
import java.util.Arrays;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.security.authentication.event.AuthenticationSuccessEvent;
import org.springframework.security.web.csrf.CsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
import org.springframework.security.web.csrf.CsrfTokenRequestHandler;
import org.springframework.security.web.csrf.DeferredCsrfToken;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import static org.springframework.web.context.request.RequestAttributes.REFERENCE_REQUEST;
@Component
public class JwtUpdateListener implements ApplicationListener<AuthenticationSuccessEvent> {
private final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private final CsrfTokenRepository tokenRepository;
private final Set<AntPathRequestMatcher> csrfUpdateRequiredPathMatchers;
private CsrfTokenRequestHandler requestHandler = new CsrfTokenRequestAttributeHandler();
@Autowired
public JwtUpdateListener(CsrfTokenRepository tokenRepository) {
Assert.notNull(tokenRepository, "tokenRepository cannot be null");
this.tokenRepository = tokenRepository;
this.csrfUpdateRequiredPathMatchers = Arrays.stream(new String[] {"/esia/auth"})
.map(AntPathRequestMatcher::new)
.collect(Collectors.toSet());
}
@Override
public void onApplicationEvent(AuthenticationSuccessEvent event) {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
HttpServletRequest request = (HttpServletRequest) Objects.requireNonNull(requestAttributes)
.resolveReference(
REFERENCE_REQUEST);
HttpServletResponse response = ((ServletRequestAttributes) requestAttributes).getResponse();
//if csrf cookie update is not required return
if (this.csrfUpdateRequiredPathMatchers.stream()
.noneMatch(matcher -> matcher.matches(request))) {
return;
}
boolean containsToken = this.tokenRepository.loadToken(request) != null;
if (containsToken) {
this.tokenRepository.saveToken(null, request, response);
DeferredCsrfToken deferredCsrfToken = this.tokenRepository.loadDeferredToken(request,
response
);
this.requestHandler.handle(request, response, deferredCsrfToken::get);
this.logger.debug("Replaced CSRF Token");
}
}
}

View file

@ -51,7 +51,7 @@
.webbpm.ervu_lkrp_ul .title { .webbpm.ervu_lkrp_ul .title {
font-size: var(--size-text-title); font-size: var(--size-text-title);
font-family: 'InterSB'; font-family: 'InterB';
padding-top: 0; padding-top: 0;
padding-bottom: var(--indent-medium); padding-bottom: var(--indent-medium);
} }
@ -279,6 +279,10 @@
white-space: nowrap; white-space: nowrap;
} }
.webbpm.ervu_lkrp_ul .data-group .description {
color: var(--color-text-secondary);
}
.webbpm.ervu_lkrp_ul .pin + .pin { .webbpm.ervu_lkrp_ul .pin + .pin {
margin-top: 12px; margin-top: 12px;
} }
@ -922,3 +926,13 @@
.webbpm.ervu_lkrp_ul .dialog-link { .webbpm.ervu_lkrp_ul .dialog-link {
cursor: pointer; cursor: pointer;
} }
.webbpm.ervu_lkrp_ul #mydata .data-group:first-child .subtitle {
margin-bottom: 0;
}
.webbpm.ervu_lkrp_ul #mydata .right-block field-set:first-child .fieldset {
padding-top: 24px;
}
.webbpm.ervu_lkrp_ul #mydata .right-block field-set:first-child .fieldset::before {
display: none;
}

View file

@ -1,4 +1,4 @@
class EsiaErrorDetail { export class EsiaErrorDetail {
private static errors: { [code: string]: string } = { private static errors: { [code: string]: string } = {
'ESIA-007071': 'Запрос персональных данных по физическим лицам может быть выполнен только с указанием согласий', 'ESIA-007071': 'Запрос персональных данных по физическим лицам может быть выполнен только с указанием согласий',
'ESIA-007055': 'Вход в систему осуществляется с неподтвержденной учетной записью', 'ESIA-007055': 'Вход в систему осуществляется с неподтвержденной учетной записью',
@ -6,7 +6,7 @@ class EsiaErrorDetail {
'ESIA-007008': 'Сервис авторизации в настоящее время не может выполнить запрос из-за большой нагрузки или технических работ на сервере', 'ESIA-007008': 'Сервис авторизации в настоящее время не может выполнить запрос из-за большой нагрузки или технических работ на сервере',
}; };
static getDescription(code: string): string { public static getDescription(code: string): string {
return this.errors[code] || 'Доступ запрещен. Обратитесь к системному администратору. Ошибка ' + code; return this.errors[code] || 'Доступ запрещен. Обратитесь к системному администратору. Ошибка ' + code;
} }
} }

View file

@ -4,6 +4,7 @@ import {Observable} from "rxjs";
import {HttpClient, HttpParams} from "@angular/common/http"; import {HttpClient, HttpParams} from "@angular/common/http";
import {MessagesService} from "@webbpm/base-package"; import {MessagesService} from "@webbpm/base-package";
import {AuthenticationService} from "../authentication.service"; import {AuthenticationService} from "../authentication.service";
import {EsiaErrorDetail} from "../EsiaErrorDetail";
@Injectable({providedIn:'root'}) @Injectable({providedIn:'root'})
export abstract class AuthGuard implements CanActivate { export abstract class AuthGuard implements CanActivate {

View file

@ -288,7 +288,7 @@
<value> <value>
<item id="7a2b1d18-0ea1-49c8-b6bf-5d91761ca0a0" removed="false"> <item id="7a2b1d18-0ea1-49c8-b6bf-5d91761ca0a0" removed="false">
<value> <value>
<simple>"font-bold"</simple> <simple>"subtitle"</simple>
</value> </value>
</item> </item>
</value> </value>
@ -338,7 +338,7 @@
<value> <value>
<item id="7a2b1d18-0ea1-49c8-b6bf-5d91761ca0a0" removed="false"> <item id="7a2b1d18-0ea1-49c8-b6bf-5d91761ca0a0" removed="false">
<value> <value>
<simple>"font-bold"</simple> <simple>"description"</simple>
</value> </value>
</item> </item>
</value> </value>
@ -432,16 +432,7 @@
<name>HB</name> <name>HB</name>
<container>true</container> <container>true</container>
<childrenReordered>false</childrenReordered> <childrenReordered>false</childrenReordered>
<scripts id="bf098f19-480e-44e4-9084-aa42955c4d0f"> <scripts id="bf098f19-480e-44e4-9084-aa42955c4d0f"/>
<properties>
<entry>
<key>visible</key>
<value>
<simple>false</simple>
</value>
</entry>
</properties>
</scripts>
<scripts id="b6068710-0f31-48ec-8e03-c0c1480a40c0"/> <scripts id="b6068710-0f31-48ec-8e03-c0c1480a40c0"/>
<scripts id="fe04d7fb-6c5b-46c4-b723-667732d81f4f"/> <scripts id="fe04d7fb-6c5b-46c4-b723-667732d81f4f"/>
<scripts id="5c566210-2a60-4048-a2d1-84c7dd023248"/> <scripts id="5c566210-2a60-4048-a2d1-84c7dd023248"/>
@ -610,7 +601,7 @@
<value> <value>
<item id="ddf92f21-aac9-4378-b2e7-3884fa0b1164" removed="false"> <item id="ddf92f21-aac9-4378-b2e7-3884fa0b1164" removed="false">
<value> <value>
<simple>"font-bold"</simple> <simple>"subtitle"</simple>
</value> </value>
</item> </item>
</value> </value>
@ -1161,7 +1152,7 @@
<value> <value>
<item id="ddf92f21-aac9-4378-b2e7-3884fa0b1164" removed="false"> <item id="ddf92f21-aac9-4378-b2e7-3884fa0b1164" removed="false">
<value> <value>
<simple>"font-bold"</simple> <simple>"subtitle"</simple>
</value> </value>
</item> </item>
</value> </value>