mirror of
https://github.com/valitydev/wachter.git
synced 2024-11-06 00:35:24 +00:00
OPS-104: Initial implementation (#1)
This commit is contained in:
parent
39ee3b689b
commit
fc68854413
2
.github/settings.yml
vendored
Normal file
2
.github/settings.yml
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
# These settings are synced to GitHub by https://probot.github.io/apps/settings/
|
||||
_extends: .github
|
10
.github/workflows/basic-linters.yml
vendored
Normal file
10
.github/workflows/basic-linters.yml
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
name: Vality basic linters
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
uses: valitydev/base-workflows/.github/workflows/basic-linters.yml@v1
|
10
.github/workflows/build.yml
vendored
Normal file
10
.github/workflows/build.yml
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
name: Build Artifact
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
uses: valitydev/base-workflow/.github/workflows/maven-service-build.yml@v1
|
18
.github/workflows/deploy.yml
vendored
Normal file
18
.github/workflows/deploy.yml
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
name: Deploy Docker Image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
- 'main'
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
uses: valitydev/base-workflow/.github/workflows/maven-service-deploy.yml@v1
|
||||
secrets:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
mm-webhook-url: ${{ secrets.MATTERMOST_WEBHOOK_URL }}
|
78
.gitignore
vendored
Normal file
78
.gitignore
vendored
Normal file
@ -0,0 +1,78 @@
|
||||
# Created by .ignore support plugin (hsz.mobi)
|
||||
.eunit
|
||||
deps
|
||||
*.o
|
||||
*.beam
|
||||
*.plt
|
||||
erl_crash.dump
|
||||
ebin/*.beam
|
||||
rel/example_project
|
||||
.concrete/DEV_MODE
|
||||
.rebar
|
||||
target/
|
||||
pom.xml.tag
|
||||
pom.xml.releaseBackup
|
||||
pom.xml.versionsBackup
|
||||
pom.xml.next
|
||||
release.properties
|
||||
dependency-reduced-pom.xml
|
||||
buildNumber.properties
|
||||
.mvn/timing.properties
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff:
|
||||
.idea/
|
||||
.idea/workspace.xml
|
||||
.idea/tasks.xml
|
||||
.idea/dictionaries
|
||||
.idea/vcs.xml
|
||||
.idea/jsLibraryMappings.xml
|
||||
|
||||
# Sensitive or high-churn files:
|
||||
.idea/dataSources.ids
|
||||
.idea/dataSources.xml
|
||||
.idea/dataSources.local.xml
|
||||
.idea/sqlDataSources.xml
|
||||
.idea/dynamic.xml
|
||||
.idea/uiDesigner.xml
|
||||
|
||||
# Gradle:
|
||||
.idea/gradle.xml
|
||||
.idea/libraries
|
||||
|
||||
# Mongo Explorer plugin:
|
||||
.idea/mongoSettings.xml
|
||||
|
||||
*.iws
|
||||
*.ipr
|
||||
*.iml
|
||||
|
||||
|
||||
# IntelliJ
|
||||
/out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
*.class
|
||||
|
||||
# Mobile Tools for Java (J2ME)
|
||||
.mtj.tmp/
|
||||
|
||||
# Package Files #
|
||||
*.jar
|
||||
*.war
|
||||
*.ear
|
||||
|
||||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||
hs_err_pid*
|
||||
env.list
|
247
pom.xml
Normal file
247
pom.xml
Normal file
@ -0,0 +1,247 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>dev.vality</groupId>
|
||||
<artifactId>service-parent-pom</artifactId>
|
||||
<version>1.0.17</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>wachter</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
<java.version>15</java.version>
|
||||
<server.port>8022</server.port>
|
||||
<management.port>8023</management.port>
|
||||
<exposed.ports>${server.port} ${management.port}</exposed.ports>
|
||||
<jackson-version>2.12.5</jackson-version>
|
||||
<spring-version>2.5.3</spring-version>
|
||||
<javax-annotation-api-version>1.3.2</javax-annotation-api-version>
|
||||
<jaxb-version>2.3.1</jaxb-version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<!--vality-->
|
||||
<dependency>
|
||||
<groupId>io.micrometer</groupId>
|
||||
<artifactId>micrometer-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.micrometer</groupId>
|
||||
<artifactId>micrometer-registry-prometheus</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>dev.vality</groupId>
|
||||
<artifactId>shared-resources</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>dev.vality</groupId>
|
||||
<artifactId>bouncer-proto</artifactId>
|
||||
<version>1.39-633ba73</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>dev.vality</groupId>
|
||||
<artifactId>org-management-proto</artifactId>
|
||||
<version>1.10-f433223</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>dev.vality.geck</groupId>
|
||||
<artifactId>serializer</artifactId>
|
||||
<version>0.0.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>dev.vality</groupId>
|
||||
<artifactId>damsel</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!--spring-->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-config</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-admin-client</artifactId>
|
||||
<version>18.0.0</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.jboss.resteasy</groupId>
|
||||
<artifactId>resteasy-client</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.jboss.resteasy</groupId>
|
||||
<artifactId>resteasy-multipart-provider</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.jboss.resteasy</groupId>
|
||||
<artifactId>resteasy-jackson2-provider</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.jboss.resteasy</groupId>
|
||||
<artifactId>resteasy-jaxb-provider</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-spring-security-adapter</artifactId>
|
||||
<version>18.0.0</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk15on</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.hibernate</groupId>
|
||||
<artifactId>hibernate-validator</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!--third party-->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>javax.servlet</groupId>
|
||||
<artifactId>javax.servlet-api</artifactId>
|
||||
<version>4.0.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>javax.annotation</groupId>
|
||||
<artifactId>javax.annotation-api</artifactId>
|
||||
<version>${javax-annotation-api-version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>javax.validation</groupId>
|
||||
<artifactId>validation-api</artifactId>
|
||||
<version>2.0.1.Final</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>javax.xml.bind</groupId>
|
||||
<artifactId>jaxb-api</artifactId>
|
||||
<version>${jaxb-version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk15on</artifactId>
|
||||
<version>1.69</version>
|
||||
</dependency>
|
||||
|
||||
<!--test-->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt</artifactId>
|
||||
<version>0.9.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-contract-wiremock</artifactId>
|
||||
<version>3.0.3</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>${project.build.directory}/maven-shared-archive-resources</directory>
|
||||
<targetPath>${project.build.directory}</targetPath>
|
||||
<includes>
|
||||
<include>Dockerfile</include>
|
||||
</includes>
|
||||
<filtering>true</filtering>
|
||||
</resource>
|
||||
<resource>
|
||||
<directory>${project.build.directory}/maven-shared-archive-resources</directory>
|
||||
<filtering>true</filtering>
|
||||
<excludes>
|
||||
<exclude>Dockerfile</exclude>
|
||||
</excludes>
|
||||
</resource>
|
||||
<resource>
|
||||
<directory>src/main/resources</directory>
|
||||
<filtering>true</filtering>
|
||||
</resource>
|
||||
</resources>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-remote-resources-plugin</artifactId>
|
||||
<version>1.6.0</version>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.apache.maven.shared</groupId>
|
||||
<artifactId>maven-filtering</artifactId>
|
||||
<version>1.3</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<configuration>
|
||||
<resourceBundles>
|
||||
<resourceBundle>dev.vality:shared-resources:${shared-resources.version}</resourceBundle>
|
||||
</resourceBundles>
|
||||
<attachToMain>false</attachToMain>
|
||||
<attachToTest>false</attachToTest>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>process</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
4
renovate.json
Normal file
4
renovate.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["local>valitydev/.github:renovate-config"]
|
||||
}
|
15
src/main/java/dev/vality/wachter/WachterApplication.java
Normal file
15
src/main/java/dev/vality/wachter/WachterApplication.java
Normal file
@ -0,0 +1,15 @@
|
||||
package dev.vality.wachter;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.web.servlet.ServletComponentScan;
|
||||
|
||||
@ServletComponentScan
|
||||
@SpringBootApplication
|
||||
public class WachterApplication extends SpringApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(WachterApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
41
src/main/java/dev/vality/wachter/client/WachterClient.java
Normal file
41
src/main/java/dev/vality/wachter/client/WachterClient.java
Normal file
@ -0,0 +1,41 @@
|
||||
package dev.vality.wachter.client;
|
||||
|
||||
import dev.vality.wachter.config.properties.WachterProperties;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.client.HttpClient;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.entity.ByteArrayEntity;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.IOException;
|
||||
import java.util.Enumeration;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class WachterClient {
|
||||
|
||||
private final HttpClient httpclient;
|
||||
|
||||
public byte[] send(HttpServletRequest request,
|
||||
byte[] contentData,
|
||||
WachterProperties.Services service) throws IOException {
|
||||
HttpPost httppost = new HttpPost(service.getUrl());
|
||||
setHeader(request, httppost);
|
||||
httppost.setEntity(new ByteArrayEntity(contentData));
|
||||
HttpResponse response = httpclient.execute(httppost);
|
||||
return EntityUtils.toByteArray(response.getEntity());
|
||||
}
|
||||
|
||||
private void setHeader(HttpServletRequest request, HttpPost httppost) {
|
||||
Enumeration<String> headerNames = request.getHeaderNames();
|
||||
if (headerNames != null) {
|
||||
while (headerNames.hasMoreElements()) {
|
||||
String next = headerNames.nextElement();
|
||||
httppost.setHeader(next, request.getHeader(next));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
package dev.vality.wachter.config;
|
||||
|
||||
import dev.vality.bouncer.decisions.ArbiterSrv;
|
||||
import dev.vality.orgmanagement.AuthContextProviderSrv;
|
||||
import dev.vality.woody.api.trace.context.metadata.user.UserIdentityEmailExtensionKit;
|
||||
import dev.vality.woody.api.trace.context.metadata.user.UserIdentityIdExtensionKit;
|
||||
import dev.vality.woody.api.trace.context.metadata.user.UserIdentityRealmExtensionKit;
|
||||
import dev.vality.woody.api.trace.context.metadata.user.UserIdentityUsernameExtensionKit;
|
||||
import dev.vality.woody.thrift.impl.http.THSpawnClientBuilder;
|
||||
import org.apache.http.HttpRequest;
|
||||
import org.apache.http.HttpRequestInterceptor;
|
||||
import org.apache.http.client.HttpClient;
|
||||
import org.apache.http.client.config.RequestConfig;
|
||||
import org.apache.http.impl.client.HttpClients;
|
||||
import org.apache.http.protocol.HTTP;
|
||||
import org.apache.http.protocol.HttpContext;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.io.Resource;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
@Configuration
|
||||
public class ApplicationConfig {
|
||||
|
||||
@Bean
|
||||
public AuthContextProviderSrv.Iface orgManagerClient(
|
||||
@Value("${orgManager.url}") Resource resource,
|
||||
@Value("${orgManager.networkTimeout}") int networkTimeout) throws IOException {
|
||||
return new THSpawnClientBuilder()
|
||||
.withNetworkTimeout(networkTimeout)
|
||||
.withMetaExtensions(List.of(
|
||||
UserIdentityIdExtensionKit.INSTANCE,
|
||||
UserIdentityEmailExtensionKit.INSTANCE,
|
||||
UserIdentityUsernameExtensionKit.INSTANCE,
|
||||
UserIdentityRealmExtensionKit.INSTANCE))
|
||||
.withAddress(resource.getURI())
|
||||
.build(AuthContextProviderSrv.Iface.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ArbiterSrv.Iface bouncerClient(
|
||||
@Value("${bouncer.url}") Resource resource,
|
||||
@Value("${bouncer.networkTimeout}") int networkTimeout) throws IOException {
|
||||
return new THSpawnClientBuilder()
|
||||
.withNetworkTimeout(networkTimeout)
|
||||
.withAddress(resource.getURI())
|
||||
.build(ArbiterSrv.Iface.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public HttpClient httpclient(@Value("${http-client.connectTimeout}") int connectTimeout,
|
||||
@Value("${http-client.connectionRequestTimeout}") int connectionRequestTimeout,
|
||||
@Value("${http-client.socketTimeout}") int socketTimeout) {
|
||||
return HttpClients.custom()
|
||||
.setDefaultRequestConfig(RequestConfig
|
||||
.custom()
|
||||
.setConnectTimeout(connectTimeout)
|
||||
.setConnectionRequestTimeout(connectionRequestTimeout)
|
||||
.setSocketTimeout(socketTimeout)
|
||||
.build())
|
||||
.addInterceptorFirst(new ContentLengthHeaderRemover())
|
||||
.build();
|
||||
}
|
||||
|
||||
private static class ContentLengthHeaderRemover implements HttpRequestInterceptor {
|
||||
@Override
|
||||
public void process(HttpRequest request, HttpContext context) {
|
||||
request.removeHeaders(HTTP.CONTENT_LEN);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
60
src/main/java/dev/vality/wachter/config/KeycloakConfig.java
Normal file
60
src/main/java/dev/vality/wachter/config/KeycloakConfig.java
Normal file
@ -0,0 +1,60 @@
|
||||
package dev.vality.wachter.config;
|
||||
|
||||
import dev.vality.wachter.config.properties.KeycloakProperties;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import org.keycloak.adapters.KeycloakConfigResolver;
|
||||
import org.keycloak.adapters.KeycloakDeployment;
|
||||
import org.keycloak.adapters.KeycloakDeploymentBuilder;
|
||||
import org.keycloak.representations.adapters.config.AdapterConfig;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@Configuration
|
||||
@ConditionalOnProperty(value = "auth.enabled", havingValue = "true")
|
||||
public class KeycloakConfig {
|
||||
|
||||
private final KeycloakProperties keycloakProperties;
|
||||
|
||||
@Bean
|
||||
public KeycloakConfigResolver keycloakConfigResolver() {
|
||||
return facade -> {
|
||||
KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(adapterConfig());
|
||||
deployment.setNotBefore(keycloakProperties.getNotBefore());
|
||||
return deployment;
|
||||
};
|
||||
}
|
||||
|
||||
private AdapterConfig adapterConfig() {
|
||||
if (StringUtils.hasLength(keycloakProperties.getRealmPublicKeyPath())) {
|
||||
keycloakProperties.setRealmPublicKey(readKeyFromFile(keycloakProperties.getRealmPublicKeyPath()));
|
||||
}
|
||||
|
||||
AdapterConfig adapterConfig = new AdapterConfig();
|
||||
adapterConfig.setRealm(keycloakProperties.getRealm());
|
||||
adapterConfig.setRealmKey(keycloakProperties.getRealmPublicKey());
|
||||
adapterConfig.setResource(keycloakProperties.getResource());
|
||||
adapterConfig.setAuthServerUrl(keycloakProperties.getAuthServerUrl());
|
||||
adapterConfig.setUseResourceRoleMappings(true);
|
||||
adapterConfig.setBearerOnly(true);
|
||||
adapterConfig.setSslRequired(keycloakProperties.getSslRequired());
|
||||
return adapterConfig;
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
private String readKeyFromFile(String filePath) {
|
||||
List<String> strings = Files.readAllLines(Paths.get(filePath));
|
||||
strings.remove(strings.size() - 1);
|
||||
strings.remove(0);
|
||||
return strings.stream().map(String::trim).collect(Collectors.joining());
|
||||
}
|
||||
|
||||
}
|
65
src/main/java/dev/vality/wachter/config/SecurityConfig.java
Normal file
65
src/main/java/dev/vality/wachter/config/SecurityConfig.java
Normal file
@ -0,0 +1,65 @@
|
||||
package dev.vality.wachter.config;
|
||||
|
||||
import org.keycloak.adapters.springsecurity.KeycloakSecurityComponents;
|
||||
import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.FilterType;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy;
|
||||
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@ComponentScan(
|
||||
basePackageClasses = KeycloakSecurityComponents.class,
|
||||
excludeFilters = @ComponentScan.Filter(
|
||||
type = FilterType.REGEX,
|
||||
pattern = "org.keycloak.adapters.springsecurity.management.HttpSessionManager"))
|
||||
@EnableGlobalMethodSecurity(prePostEnabled = true, proxyTargetClass = true)
|
||||
@ConditionalOnProperty(value = "auth.enabled", havingValue = "true")
|
||||
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
|
||||
|
||||
@Bean
|
||||
@Override
|
||||
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
|
||||
return new NullAuthenticatedSessionStrategy();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
super.configure(http);
|
||||
http.cors().and()
|
||||
.csrf().disable()
|
||||
.authorizeRequests()
|
||||
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
|
||||
.antMatchers(HttpMethod.GET, "/**/health").permitAll()
|
||||
.anyRequest().authenticated();
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public void configureGlobal(AuthenticationManagerBuilder auth) {
|
||||
auth.authenticationProvider(keycloakAuthenticationProvider());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CorsConfigurationSource corsConfigurationSource() {
|
||||
CorsConfiguration configuration = new CorsConfiguration();
|
||||
configuration.applyPermitDefaultValues();
|
||||
configuration.addAllowedMethod(HttpMethod.PUT);
|
||||
configuration.addAllowedMethod(HttpMethod.DELETE);
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", configuration);
|
||||
return source;
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package dev.vality.wachter.config;
|
||||
|
||||
import org.apache.thrift.protocol.TBinaryProtocol;
|
||||
import org.apache.thrift.protocol.TProtocolFactory;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class ThriftGatewayConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(TProtocolFactory.class)
|
||||
TProtocolFactory thriftProtocolFactory() {
|
||||
return new TBinaryProtocol.Factory();
|
||||
}
|
||||
|
||||
}
|
103
src/main/java/dev/vality/wachter/config/WebConfig.java
Normal file
103
src/main/java/dev/vality/wachter/config/WebConfig.java
Normal file
@ -0,0 +1,103 @@
|
||||
package dev.vality.wachter.config;
|
||||
|
||||
import dev.vality.wachter.utils.DeadlineUtil;
|
||||
import dev.vality.woody.api.flow.WFlow;
|
||||
import dev.vality.woody.api.trace.context.metadata.user.UserIdentityEmailExtensionKit;
|
||||
import dev.vality.woody.api.trace.context.metadata.user.UserIdentityIdExtensionKit;
|
||||
import dev.vality.woody.api.trace.context.metadata.user.UserIdentityRealmExtensionKit;
|
||||
import dev.vality.woody.api.trace.context.metadata.user.UserIdentityUsernameExtensionKit;
|
||||
import org.keycloak.KeycloakSecurityContext;
|
||||
import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import javax.servlet.Filter;
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.security.Principal;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
import static dev.vality.woody.api.trace.ContextUtils.setCustomMetadataValue;
|
||||
import static dev.vality.woody.api.trace.ContextUtils.setDeadline;
|
||||
|
||||
@Configuration
|
||||
@SuppressWarnings({"ParameterName", "LocalVariableName"})
|
||||
public class WebConfig {
|
||||
|
||||
@Bean
|
||||
public FilterRegistrationBean woodyFilter() {
|
||||
WFlow woodyFlow = new WFlow();
|
||||
Filter filter = new OncePerRequestFilter() {
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
FilterChain filterChain) throws ServletException, IOException {
|
||||
woodyFlow.createServiceFork(
|
||||
() -> {
|
||||
try {
|
||||
if (request.getUserPrincipal() != null) {
|
||||
addWoodyContext(request.getUserPrincipal());
|
||||
}
|
||||
|
||||
setWoodyDeadline(request);
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
} catch (IOException | ServletException e) {
|
||||
sneakyThrow(e);
|
||||
}
|
||||
}
|
||||
)
|
||||
.run();
|
||||
}
|
||||
|
||||
private <E extends Throwable, T> T sneakyThrow(Throwable t) throws E {
|
||||
throw (E) t;
|
||||
}
|
||||
};
|
||||
|
||||
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
|
||||
filterRegistrationBean.setFilter(filter);
|
||||
filterRegistrationBean.setOrder(-50);
|
||||
filterRegistrationBean.setName("woodyFilter");
|
||||
filterRegistrationBean.addUrlPatterns("*");
|
||||
return filterRegistrationBean;
|
||||
}
|
||||
|
||||
private void addWoodyContext(Principal principal) {
|
||||
KeycloakSecurityContext keycloakSecurityContext =
|
||||
((KeycloakAuthenticationToken) principal).getAccount().getKeycloakSecurityContext();
|
||||
AccessToken accessToken = keycloakSecurityContext.getToken();
|
||||
|
||||
setCustomMetadataValue(UserIdentityIdExtensionKit.KEY, accessToken.getSubject());
|
||||
setCustomMetadataValue(UserIdentityUsernameExtensionKit.KEY, accessToken.getPreferredUsername());
|
||||
setCustomMetadataValue(UserIdentityEmailExtensionKit.KEY, accessToken.getEmail());
|
||||
setCustomMetadataValue(UserIdentityRealmExtensionKit.KEY, keycloakSecurityContext.getRealm());
|
||||
}
|
||||
|
||||
private void setWoodyDeadline(HttpServletRequest request) {
|
||||
String xRequestDeadline = request.getHeader("X-Request-Deadline");
|
||||
String xRequestId = request.getHeader("X-Request-ID");
|
||||
if (xRequestDeadline != null) {
|
||||
setDeadline(getInstant(xRequestDeadline, xRequestId));
|
||||
}
|
||||
}
|
||||
|
||||
private Instant getInstant(String xRequestDeadline, String xRequestId) {
|
||||
if (DeadlineUtil.containsRelativeValues(xRequestDeadline, xRequestId)) {
|
||||
return Instant.now()
|
||||
.plus(DeadlineUtil.extractMilliseconds(xRequestDeadline, xRequestId), ChronoUnit.MILLIS)
|
||||
.plus(DeadlineUtil.extractSeconds(xRequestDeadline, xRequestId), ChronoUnit.MILLIS)
|
||||
.plus(DeadlineUtil.extractMinutes(xRequestDeadline, xRequestId), ChronoUnit.MILLIS);
|
||||
} else {
|
||||
return Instant.parse(xRequestDeadline);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
package dev.vality.wachter.config.properties;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Component
|
||||
@Validated
|
||||
@ConfigurationProperties(prefix = "bouncer")
|
||||
public class BouncerProperties {
|
||||
|
||||
@NotEmpty
|
||||
private String deploymentId;
|
||||
@NotEmpty
|
||||
private String authMethod;
|
||||
@NotEmpty
|
||||
private String realm;
|
||||
@NotEmpty
|
||||
private String ruleSetId;
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package dev.vality.wachter.config.properties;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Component
|
||||
@Validated
|
||||
@ConfigurationProperties(prefix = "keycloak")
|
||||
public class KeycloakProperties {
|
||||
|
||||
@NotEmpty
|
||||
private String realm;
|
||||
@NotEmpty
|
||||
private String authServerUrl;
|
||||
@NotEmpty
|
||||
private String resource;
|
||||
|
||||
private Integer notBefore;
|
||||
|
||||
@NotEmpty
|
||||
private String sslRequired;
|
||||
|
||||
private String realmPublicKey;
|
||||
|
||||
private String realmPublicKeyPath;
|
||||
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package dev.vality.wachter.config.properties;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Component
|
||||
@Validated
|
||||
@ConfigurationProperties(prefix = "wachter")
|
||||
public class WachterProperties {
|
||||
|
||||
private String serviceHeader;
|
||||
private Map<String, Services> services;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public static class Services {
|
||||
|
||||
private String name;
|
||||
private String url;
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
package dev.vality.wachter.controller;
|
||||
|
||||
import dev.vality.wachter.exeptions.AuthorizationException;
|
||||
import dev.vality.wachter.exeptions.WachterException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.client.HttpClientErrorException;
|
||||
|
||||
import java.net.http.HttpTimeoutException;
|
||||
|
||||
@Slf4j
|
||||
@RestControllerAdvice
|
||||
@RequiredArgsConstructor
|
||||
public class ErrorControllerAdvice {
|
||||
|
||||
@ExceptionHandler({WachterException.class})
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public Object handleBadRequestException(WachterException e) {
|
||||
log.warn("<- Res [400]: Not valid request", e);
|
||||
return e.getMessage();
|
||||
}
|
||||
|
||||
|
||||
@ExceptionHandler({AccessDeniedException.class})
|
||||
@ResponseStatus(HttpStatus.FORBIDDEN)
|
||||
public void handleAccessDeniedException(AccessDeniedException e) {
|
||||
log.warn("<- Res [403]: Request denied access", e);
|
||||
}
|
||||
|
||||
@ExceptionHandler({AuthorizationException.class})
|
||||
@ResponseStatus(HttpStatus.FORBIDDEN)
|
||||
public void handleAccessDeniedException(AuthorizationException e) {
|
||||
log.warn("<- Res [403]: Request denied access", e);
|
||||
}
|
||||
|
||||
@ExceptionHandler(HttpClientErrorException.class)
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public void handleHttpClientErrorException(HttpClientErrorException e) {
|
||||
log.error("<- Res [500]: Error with using inner http client, code={}, body={}",
|
||||
e.getStatusCode(), e.getResponseBodyAsString(), e);
|
||||
}
|
||||
|
||||
@ExceptionHandler(HttpTimeoutException.class)
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public void handleHttpTimeoutException(HttpTimeoutException e) {
|
||||
log.error("<- Res [500]: Timeout with using inner http client", e);
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public void handleException(Exception e) {
|
||||
log.error("<- Res [500]: Unrecognized inner error", e);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
package dev.vality.wachter.controller;
|
||||
|
||||
import dev.vality.wachter.service.WachterService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.thrift.TException;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.IOException;
|
||||
|
||||
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("")
|
||||
public class WachterController {
|
||||
|
||||
private final WachterService wachterService;
|
||||
|
||||
@PostMapping("/wachter")
|
||||
public byte[] getRequest(HttpServletRequest request) throws IOException, TException {
|
||||
return wachterService.process(request);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
package dev.vality.wachter.exeptions;
|
||||
|
||||
public class AuthorizationException extends WachterException {
|
||||
|
||||
public AuthorizationException(String s) {
|
||||
super(s);
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package dev.vality.wachter.exeptions;
|
||||
|
||||
public class BouncerException extends WachterException {
|
||||
|
||||
public BouncerException(String s) {
|
||||
super(s);
|
||||
}
|
||||
|
||||
public BouncerException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
package dev.vality.wachter.exeptions;
|
||||
|
||||
public class DeadlineException extends WachterException {
|
||||
|
||||
public DeadlineException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
package dev.vality.wachter.exeptions;
|
||||
|
||||
public class OrgManagerException extends WachterException {
|
||||
|
||||
public OrgManagerException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package dev.vality.wachter.exeptions;
|
||||
|
||||
public class WachterException extends RuntimeException {
|
||||
|
||||
public WachterException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public WachterException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
32
src/main/java/dev/vality/wachter/mapper/ServiceMapper.java
Normal file
32
src/main/java/dev/vality/wachter/mapper/ServiceMapper.java
Normal file
@ -0,0 +1,32 @@
|
||||
package dev.vality.wachter.mapper;
|
||||
|
||||
import dev.vality.wachter.config.properties.WachterProperties;
|
||||
import dev.vality.wachter.exeptions.WachterException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class ServiceMapper {
|
||||
|
||||
private final WachterProperties wachterProperties;
|
||||
|
||||
public WachterProperties.Services getService(HttpServletRequest request) {
|
||||
if (request.getHeader(wachterProperties.getServiceHeader()) == null) {
|
||||
throw new WachterException(
|
||||
String.format("Header \"%s\" must be set", wachterProperties.getServiceHeader()));
|
||||
}
|
||||
WachterProperties.Services service = wachterProperties.getServices()
|
||||
.get(request.getHeader(wachterProperties.getServiceHeader()));
|
||||
|
||||
if (service == null) {
|
||||
throw new WachterException(
|
||||
String.format("Service \"%s\" not found in configuration",
|
||||
request.getHeader(wachterProperties.getServiceHeader())));
|
||||
}
|
||||
return service;
|
||||
}
|
||||
|
||||
}
|
19
src/main/java/dev/vality/wachter/security/AccessData.java
Normal file
19
src/main/java/dev/vality/wachter/security/AccessData.java
Normal file
@ -0,0 +1,19 @@
|
||||
package dev.vality.wachter.security;
|
||||
|
||||
import dev.vality.wachter.config.properties.WachterProperties;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
@Builder
|
||||
@Data
|
||||
public class AccessData {
|
||||
|
||||
private final String operationId;
|
||||
private final String partyId;
|
||||
private final long tokenExpirationSec;
|
||||
private final String tokenId;
|
||||
private final String userId;
|
||||
private final String userEmail;
|
||||
private final WachterProperties.Services service;
|
||||
|
||||
}
|
49
src/main/java/dev/vality/wachter/security/AccessService.java
Normal file
49
src/main/java/dev/vality/wachter/security/AccessService.java
Normal file
@ -0,0 +1,49 @@
|
||||
package dev.vality.wachter.security;
|
||||
|
||||
import dev.vality.wachter.exeptions.AuthorizationException;
|
||||
import dev.vality.wachter.exeptions.BouncerException;
|
||||
import dev.vality.wachter.service.BouncerService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class AccessService {
|
||||
|
||||
private final BouncerService bouncerService;
|
||||
|
||||
@Value("${bouncer.auth.enabled}")
|
||||
private boolean authEnabled;
|
||||
|
||||
public void checkUserAccess(AccessData accessData) {
|
||||
log.info("Check the {} rights to perform the operation {} in service {}",
|
||||
accessData.getUserEmail(),
|
||||
accessData.getOperationId(),
|
||||
accessData.getService().getName());
|
||||
var resolution = bouncerService.getResolution(accessData);
|
||||
switch (resolution.getSetField()) {
|
||||
case FORBIDDEN, RESTRICTED -> {
|
||||
if (authEnabled) {
|
||||
throw new AuthorizationException(
|
||||
String.format("No rights for %s to perform %s in service %s",
|
||||
accessData.getUserEmail(),
|
||||
accessData.getOperationId(),
|
||||
accessData.getService().getName()));
|
||||
} else {
|
||||
log.warn("No rights for {} to perform {} in service {}",
|
||||
accessData.getUserEmail(),
|
||||
accessData.getOperationId(),
|
||||
accessData.getService().getName());
|
||||
}
|
||||
}
|
||||
case ALLOWED -> log.info("Rights for {} to perform {} in service {} are allowed",
|
||||
accessData.getUserEmail(),
|
||||
accessData.getOperationId(),
|
||||
accessData.getService().getName());
|
||||
default -> throw new BouncerException(String.format("Resolution %s cannot be processed", resolution));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
package dev.vality.wachter.security;
|
||||
|
||||
import dev.vality.bouncer.base.Entity;
|
||||
import dev.vality.bouncer.context.v1.*;
|
||||
import dev.vality.bouncer.ctx.ContextFragmentType;
|
||||
import dev.vality.bouncer.decisions.Context;
|
||||
import dev.vality.wachter.config.properties.BouncerProperties;
|
||||
import dev.vality.wachter.service.KeycloakService;
|
||||
import dev.vality.wachter.service.OrgManagerService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import org.apache.thrift.TSerializer;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
public class BouncerContextFactory {
|
||||
|
||||
private final BouncerProperties bouncerProperties;
|
||||
private final OrgManagerService orgManagerService;
|
||||
private final KeycloakService keycloakService;
|
||||
|
||||
@SneakyThrows
|
||||
public Context buildContext(AccessData accessData) {
|
||||
var contextFragment = buildContextFragment(accessData);
|
||||
var serializer = new TSerializer();
|
||||
var fragment = new dev.vality.bouncer.ctx.ContextFragment()
|
||||
.setType(ContextFragmentType.v1_thrift_binary)
|
||||
.setContent(serializer.serialize(contextFragment));
|
||||
var userFragment = orgManagerService.getUserAuthContext(
|
||||
keycloakService.getAccessToken().getSubject());
|
||||
var context = new Context();
|
||||
context.putToFragments(accessData.getService().getName(), fragment);
|
||||
context.putToFragments("user", userFragment);
|
||||
return context;
|
||||
}
|
||||
|
||||
private ContextFragment buildContextFragment(AccessData accessData) {
|
||||
var env = buildEnvironment();
|
||||
return new ContextFragment()
|
||||
.setAuth(buildAuth(accessData))
|
||||
.setEnv(env);
|
||||
}
|
||||
|
||||
private Auth buildAuth(AccessData accessData) {
|
||||
var auth = new Auth();
|
||||
Set<AuthScope> authScopeSet = new HashSet<>();
|
||||
authScopeSet.add(new AuthScope()
|
||||
.setParty(new Entity().setId(accessData.getPartyId())));
|
||||
return auth.setToken(new Token().setId(accessData.getTokenId()))
|
||||
.setMethod(bouncerProperties.getAuthMethod())
|
||||
.setExpiration(Instant.ofEpochSecond(accessData.getTokenExpirationSec()).toString())
|
||||
.setScope(authScopeSet);
|
||||
}
|
||||
|
||||
private Environment buildEnvironment() {
|
||||
var deployment = new Deployment()
|
||||
.setId(bouncerProperties.getDeploymentId());
|
||||
return new Environment()
|
||||
.setDeployment(deployment)
|
||||
.setNow(Instant.now().toString());
|
||||
}
|
||||
|
||||
}
|
37
src/main/java/dev/vality/wachter/service/BouncerService.java
Normal file
37
src/main/java/dev/vality/wachter/service/BouncerService.java
Normal file
@ -0,0 +1,37 @@
|
||||
package dev.vality.wachter.service;
|
||||
|
||||
import dev.vality.bouncer.decisions.ArbiterSrv;
|
||||
import dev.vality.bouncer.decisions.Resolution;
|
||||
import dev.vality.wachter.config.properties.BouncerProperties;
|
||||
import dev.vality.wachter.exeptions.BouncerException;
|
||||
import dev.vality.wachter.security.AccessData;
|
||||
import dev.vality.wachter.security.BouncerContextFactory;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.thrift.TException;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class BouncerService {
|
||||
|
||||
private final BouncerProperties bouncerProperties;
|
||||
private final BouncerContextFactory bouncerContextFactory;
|
||||
private final ArbiterSrv.Iface bouncerClient;
|
||||
|
||||
public Resolution getResolution(AccessData accessData) {
|
||||
log.debug("Check access with bouncer context");
|
||||
var context = bouncerContextFactory.buildContext(accessData);
|
||||
log.debug("Built thrift context: {}", context);
|
||||
try {
|
||||
var judge = bouncerClient.judge(bouncerProperties.getRuleSetId(), context);
|
||||
log.debug("Have judge: {}", judge);
|
||||
var resolution = judge.getResolution();
|
||||
log.debug("Resolution: {}", resolution);
|
||||
return resolution;
|
||||
} catch (TException e) {
|
||||
throw new BouncerException("Error while call bouncer", e);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
package dev.vality.wachter.service;
|
||||
|
||||
import org.keycloak.KeycloakPrincipal;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class KeycloakService {
|
||||
|
||||
public String getPartyId() {
|
||||
return ((KeycloakPrincipal) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getName();
|
||||
}
|
||||
|
||||
public AccessToken getAccessToken() {
|
||||
KeycloakPrincipal keycloakPrincipal = (KeycloakPrincipal) SecurityContextHolder.getContext()
|
||||
.getAuthentication()
|
||||
.getPrincipal();
|
||||
|
||||
return keycloakPrincipal.getKeycloakSecurityContext().getToken();
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package dev.vality.wachter.service;
|
||||
|
||||
import dev.vality.bouncer.ctx.ContextFragment;
|
||||
import dev.vality.orgmanagement.AuthContextProviderSrv;
|
||||
import dev.vality.wachter.exeptions.OrgManagerException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class OrgManagerService {
|
||||
|
||||
private final AuthContextProviderSrv.Iface orgManagerClient;
|
||||
|
||||
public ContextFragment getUserAuthContext(String userId) {
|
||||
try {
|
||||
return orgManagerClient.getUserContext(userId);
|
||||
} catch (Exception e) {
|
||||
throw new OrgManagerException(
|
||||
String.format("Can't get user auth context on orgManager call: userId = %s", userId),
|
||||
e);
|
||||
}
|
||||
}
|
||||
}
|
52
src/main/java/dev/vality/wachter/service/WachterService.java
Normal file
52
src/main/java/dev/vality/wachter/service/WachterService.java
Normal file
@ -0,0 +1,52 @@
|
||||
package dev.vality.wachter.service;
|
||||
|
||||
import dev.vality.wachter.client.WachterClient;
|
||||
import dev.vality.wachter.security.AccessData;
|
||||
import dev.vality.wachter.security.AccessService;
|
||||
import dev.vality.wachter.mapper.ServiceMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.thrift.TException;
|
||||
import org.apache.tomcat.util.http.fileupload.IOUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
import static dev.vality.wachter.utils.MethodNameReader.getMethodName;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class WachterService {
|
||||
|
||||
private final KeycloakService keycloakService;
|
||||
private final AccessService accessService;
|
||||
private final WachterClient wachterClient;
|
||||
private final ServiceMapper serviceMapper;
|
||||
|
||||
|
||||
public byte[] process(HttpServletRequest request) throws IOException, TException {
|
||||
byte[] contentData = getContentData(request);
|
||||
var methodName = getMethodName(contentData);
|
||||
var partyID = keycloakService.getPartyId();
|
||||
var token = keycloakService.getAccessToken();
|
||||
var service = serviceMapper.getService(request);
|
||||
accessService.checkUserAccess(AccessData.builder()
|
||||
.operationId(methodName)
|
||||
.partyId(partyID)
|
||||
.tokenExpirationSec(token.getExp())
|
||||
.tokenId(token.getId())
|
||||
.userId(token.getSubject())
|
||||
.userEmail(token.getEmail())
|
||||
.service(service)
|
||||
.build());
|
||||
return wachterClient.send(request, contentData, service);
|
||||
}
|
||||
|
||||
private byte[] getContentData(HttpServletRequest request) throws IOException {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
IOUtils.copy(request.getInputStream(), baos);
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
}
|
120
src/main/java/dev/vality/wachter/utils/DeadlineUtil.java
Normal file
120
src/main/java/dev/vality/wachter/utils/DeadlineUtil.java
Normal file
@ -0,0 +1,120 @@
|
||||
package dev.vality.wachter.utils;
|
||||
|
||||
import dev.vality.wachter.exeptions.DeadlineException;
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@UtilityClass
|
||||
@SuppressWarnings("ParameterName")
|
||||
public class DeadlineUtil {
|
||||
|
||||
private static final String FLOATING_NUMBER_REGEXP = "[0-9]+([.][0-9]+)?";
|
||||
private static final String MIN_REGEXP = "(?!ms)[m]";
|
||||
private static final String SEC_REGEXP = "[s]";
|
||||
private static final String MILLISECOND_REGEXP = "[m][s]";
|
||||
|
||||
public static boolean containsRelativeValues(String xRequestDeadline, String xRequestId) {
|
||||
return extractMinutes(xRequestDeadline, xRequestId) + extractSeconds(xRequestDeadline, xRequestId) +
|
||||
extractMilliseconds(xRequestDeadline, xRequestId) > 0;
|
||||
}
|
||||
|
||||
public static Long extractMinutes(String xRequestDeadline, String xRequestId) {
|
||||
String format = "minutes";
|
||||
|
||||
checkNegativeValues(
|
||||
xRequestDeadline,
|
||||
xRequestId,
|
||||
"([-]" + FLOATING_NUMBER_REGEXP + MIN_REGEXP + ")",
|
||||
format);
|
||||
|
||||
Double minutes = extractValue(
|
||||
xRequestDeadline,
|
||||
"(" + FLOATING_NUMBER_REGEXP + MIN_REGEXP + ")",
|
||||
xRequestId,
|
||||
format);
|
||||
|
||||
return Optional.ofNullable(minutes).map(min -> min * 60000.0).map(Double::longValue).orElse(0L);
|
||||
}
|
||||
|
||||
public static Long extractSeconds(String xRequestDeadline, String xRequestId) {
|
||||
String format = "seconds";
|
||||
|
||||
checkNegativeValues(
|
||||
xRequestDeadline,
|
||||
xRequestId,
|
||||
"([-]" + FLOATING_NUMBER_REGEXP + SEC_REGEXP + ")",
|
||||
format);
|
||||
|
||||
Double seconds = extractValue(
|
||||
xRequestDeadline,
|
||||
"(" + FLOATING_NUMBER_REGEXP + SEC_REGEXP + ")",
|
||||
xRequestId,
|
||||
format);
|
||||
|
||||
return Optional.ofNullable(seconds).map(s -> s * 1000.0).map(Double::longValue).orElse(0L);
|
||||
}
|
||||
|
||||
public static Long extractMilliseconds(String xRequestDeadline, String xRequestId) {
|
||||
String format = "milliseconds";
|
||||
|
||||
checkNegativeValues(
|
||||
xRequestDeadline,
|
||||
xRequestId,
|
||||
"([-]" + FLOATING_NUMBER_REGEXP + MILLISECOND_REGEXP + ")",
|
||||
format);
|
||||
|
||||
Double milliseconds = extractValue(
|
||||
xRequestDeadline,
|
||||
"(" + FLOATING_NUMBER_REGEXP + MILLISECOND_REGEXP + ")",
|
||||
xRequestId,
|
||||
format);
|
||||
|
||||
if (milliseconds != null && Math.ceil(milliseconds % 1) > 0) {
|
||||
throw new DeadlineException(
|
||||
String.format("Deadline 'milliseconds' parameter can have only integer value, xRequestId=%s ",
|
||||
xRequestId));
|
||||
}
|
||||
|
||||
return Optional.ofNullable(milliseconds).map(Double::longValue).orElse(0L);
|
||||
}
|
||||
|
||||
private static void checkNegativeValues(String xRequestDeadline, String xRequestId, String regex, String format) {
|
||||
if (!match(regex, xRequestDeadline).isEmpty()) {
|
||||
throw new DeadlineException(
|
||||
String.format("Deadline '%s' parameter has negative value, xRequestId=%s ", format, xRequestId));
|
||||
}
|
||||
}
|
||||
|
||||
private static Double extractValue(String xRequestDeadline, String formatRegex, String xRequestId, String format) {
|
||||
String numberRegex = "(" + FLOATING_NUMBER_REGEXP + ")";
|
||||
|
||||
List<String> doubles = new ArrayList<>();
|
||||
for (String string : match(formatRegex, xRequestDeadline)) {
|
||||
doubles.addAll(match(numberRegex, string));
|
||||
}
|
||||
if (doubles.size() > 1) {
|
||||
throw new DeadlineException(
|
||||
String.format("Deadline '%s' parameter has a few relative value, xRequestId=%s ", format,
|
||||
xRequestId));
|
||||
}
|
||||
if (doubles.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return Double.valueOf(doubles.get(0));
|
||||
}
|
||||
|
||||
private static List<String> match(String regex, String data) {
|
||||
Pattern pattern = Pattern.compile(regex);
|
||||
Matcher matcher = pattern.matcher(data);
|
||||
List<String> strings = new ArrayList<>();
|
||||
while (matcher.find()) {
|
||||
strings.add(matcher.group());
|
||||
}
|
||||
return strings;
|
||||
}
|
||||
}
|
23
src/main/java/dev/vality/wachter/utils/MethodNameReader.java
Normal file
23
src/main/java/dev/vality/wachter/utils/MethodNameReader.java
Normal file
@ -0,0 +1,23 @@
|
||||
package dev.vality.wachter.utils;
|
||||
|
||||
import org.apache.thrift.TException;
|
||||
import org.apache.thrift.protocol.TBinaryProtocol;
|
||||
import org.apache.thrift.protocol.TMessage;
|
||||
import org.apache.thrift.protocol.TProtocol;
|
||||
import org.apache.thrift.transport.TMemoryInputTransport;
|
||||
import org.apache.thrift.transport.TTransportException;
|
||||
|
||||
public class MethodNameReader {
|
||||
|
||||
public static String getMethodName(byte[] thriftBody) throws TException {
|
||||
TProtocol protocol = createProtocol(thriftBody);
|
||||
TMessage message = protocol.readMessageBegin();
|
||||
protocol.readMessageEnd();
|
||||
return message.name;
|
||||
}
|
||||
|
||||
private static TProtocol createProtocol(byte[] thriftBody) throws TTransportException {
|
||||
return new TBinaryProtocol.Factory().getProtocol(new TMemoryInputTransport(thriftBody));
|
||||
}
|
||||
|
||||
}
|
101
src/main/resources/application.yml
Normal file
101
src/main/resources/application.yml
Normal file
@ -0,0 +1,101 @@
|
||||
server:
|
||||
port: '@server.port@'
|
||||
management:
|
||||
security:
|
||||
flag: false
|
||||
server:
|
||||
port: '@management.port@'
|
||||
metrics:
|
||||
tags:
|
||||
application: '@project.name@'
|
||||
export:
|
||||
prometheus:
|
||||
enabled: true
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
metrics:
|
||||
enabled: true
|
||||
prometheus:
|
||||
enabled: true
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,prometheus
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: '@project.name@'
|
||||
output:
|
||||
ansi:
|
||||
enabled: always
|
||||
info:
|
||||
version: '@project.version@'
|
||||
stage: dev
|
||||
|
||||
wachter:
|
||||
serviceHeader: Service
|
||||
services:
|
||||
messages:
|
||||
name: messages
|
||||
url: http://localhost:8097/v1/messages
|
||||
automaton:
|
||||
name: automaton
|
||||
url: http://localhost:8022/v1/automaton
|
||||
repairer:
|
||||
name: repairer
|
||||
url: http://localhost:8022/v1/repair/withdrawal/session
|
||||
claimManagement:
|
||||
name: claimManagement
|
||||
url: http://localhost:8097/v1/cm
|
||||
fistfulAdmin:
|
||||
name: fistfulAdmin
|
||||
url: http://localhost:8022/v1/admin
|
||||
fistfulStatistics:
|
||||
name: fistfulStatistics
|
||||
url: http://localhost:8022/fistful/stat
|
||||
fileStorage:
|
||||
name: fileStorage
|
||||
url: http://localhost:8022/file_storage
|
||||
deanonimus:
|
||||
name: deanonimus
|
||||
url: http://localhost:8022/deanonimus
|
||||
merchantStatistics:
|
||||
name: merchantStatistics
|
||||
url: http://localhost:8022/stat
|
||||
paymentProcessing:
|
||||
name: paymentProcessing
|
||||
url: http://localhost:8022/v1/processing/invoicing
|
||||
domain:
|
||||
name: domain
|
||||
url: http://localhost:8022/v1/domain/repository
|
||||
|
||||
|
||||
http-client:
|
||||
connectTimeout: 10000
|
||||
connectionRequestTimeout: 10000
|
||||
socketTimeout: 10000
|
||||
|
||||
orgManager:
|
||||
url: http://localhost:8022/org/v1/auth-context
|
||||
networkTimeout: 5000
|
||||
|
||||
bouncer:
|
||||
url: http://localhost:8022/v1/arbiter
|
||||
networkTimeout: 10000
|
||||
deployment-id: production
|
||||
auth-method: SessionToken
|
||||
realm: external
|
||||
rule-set-id: change_it
|
||||
auth:
|
||||
enabled: true
|
||||
|
||||
keycloak:
|
||||
realm: internal
|
||||
auth-server-url: http://keycloak:8080/auth/
|
||||
resource: koffing
|
||||
not-before: 0
|
||||
ssl-required: none
|
||||
realm-public-key:
|
||||
|
||||
auth.enabled: true
|
@ -0,0 +1,45 @@
|
||||
package dev.vality.wachter.auth;
|
||||
|
||||
import dev.vality.wachter.auth.utils.JwtTokenBuilder;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.*;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.Base64;
|
||||
import java.util.Properties;
|
||||
|
||||
@Configuration
|
||||
public class JwtTokenTestConfiguration {
|
||||
|
||||
@Bean
|
||||
public static PropertySourcesPlaceholderConfigurer properties(KeyPair keyPair)
|
||||
throws NoSuchAlgorithmException, InvalidKeySpecException, IOException {
|
||||
KeyFactory fact = KeyFactory.getInstance("RSA");
|
||||
X509EncodedKeySpec spec = fact.getKeySpec(keyPair.getPublic(), X509EncodedKeySpec.class);
|
||||
String publicKey = Base64.getEncoder().encodeToString(spec.getEncoded());
|
||||
PropertySourcesPlaceholderConfigurer pspc = new PropertySourcesPlaceholderConfigurer();
|
||||
Properties properties = new Properties();
|
||||
properties.load(new ClassPathResource("application.yml").getInputStream());
|
||||
properties.setProperty("keycloak.realm-public-key", publicKey);
|
||||
pspc.setProperties(properties);
|
||||
pspc.setLocalOverride(true);
|
||||
return pspc;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public JwtTokenBuilder jwtTokenBuilder(KeyPair keyPair) {
|
||||
return new JwtTokenBuilder(keyPair.getPrivate());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public KeyPair keyPair() throws GeneralSecurityException {
|
||||
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
|
||||
keyGen.initialize(2048);
|
||||
return keyGen.generateKeyPair();
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package dev.vality.wachter.auth;
|
||||
|
||||
import dev.vality.wachter.auth.utils.JwtTokenBuilder;
|
||||
import dev.vality.wachter.auth.utils.KeycloakOpenIdStub;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class KeycloakOpenIdTestConfiguration {
|
||||
|
||||
@Bean
|
||||
public KeycloakOpenIdStub keycloakOpenIdStub(@Value("${keycloak.auth-server-url}") String keycloakAuthServerUrl,
|
||||
@Value("${keycloak.realm}") String keycloakRealm,
|
||||
JwtTokenBuilder jwtTokenBuilder) {
|
||||
return new KeycloakOpenIdStub(keycloakAuthServerUrl, keycloakRealm, jwtTokenBuilder);
|
||||
}
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
package dev.vality.wachter.auth.utils;
|
||||
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.SignatureAlgorithm;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.security.PrivateKey;
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
public class JwtTokenBuilder {
|
||||
|
||||
public static final String DEFAULT_USERNAME = "Darth Vader";
|
||||
|
||||
public static final String DEFAULT_EMAIL = "darkside-the-best@mail.com";
|
||||
|
||||
private final String userId;
|
||||
|
||||
private final String username;
|
||||
|
||||
private final String email;
|
||||
|
||||
private final PrivateKey privateKey;
|
||||
|
||||
public JwtTokenBuilder(PrivateKey privateKey) {
|
||||
this(UUID.randomUUID().toString(), DEFAULT_USERNAME, DEFAULT_EMAIL, privateKey);
|
||||
}
|
||||
|
||||
public JwtTokenBuilder(String userId, String username, String email, PrivateKey privateKey) {
|
||||
this.userId = userId;
|
||||
this.username = username;
|
||||
this.email = email;
|
||||
this.privateKey = privateKey;
|
||||
}
|
||||
|
||||
public String generateJwtWithRoles(String issuer, String... roles) {
|
||||
long iat = Instant.now().getEpochSecond();
|
||||
long exp = iat + 60 * 10;
|
||||
return generateJwtWithRoles(iat, exp, issuer, roles);
|
||||
}
|
||||
|
||||
public String generateJwtWithRoles(long iat, long exp, String issuer, String... roles) {
|
||||
String payload;
|
||||
try {
|
||||
payload = new JSONObject()
|
||||
.put("jti", UUID.randomUUID().toString())
|
||||
.put("exp", exp)
|
||||
.put("nbf", "0")
|
||||
.put("iat", iat)
|
||||
.put("iss", issuer)
|
||||
.put("aud", "private-api")
|
||||
.put("sub", userId)
|
||||
.put("typ", "Bearer")
|
||||
.put("azp", "private-api")
|
||||
.put("resource_access", new JSONObject()
|
||||
.put("common-api", new JSONObject()
|
||||
.put("roles", new JSONArray(roles))))
|
||||
.put("preferred_username", username)
|
||||
.put("email", email).toString();
|
||||
} catch (JSONException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
String jwt = Jwts.builder()
|
||||
.setPayload(payload)
|
||||
.signWith(SignatureAlgorithm.RS256, privateKey)
|
||||
.compact();
|
||||
return jwt;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
package dev.vality.wachter.auth.utils;
|
||||
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.*;
|
||||
|
||||
public class KeycloakOpenIdStub {
|
||||
|
||||
private final String keycloakRealm;
|
||||
private final String issuer;
|
||||
private final String openidConfig;
|
||||
private final JwtTokenBuilder jwtTokenBuilder;
|
||||
|
||||
public KeycloakOpenIdStub(String keycloakAuthServerUrl, String keycloakRealm, JwtTokenBuilder jwtTokenBuilder) {
|
||||
this.keycloakRealm = keycloakRealm;
|
||||
this.jwtTokenBuilder = jwtTokenBuilder;
|
||||
this.issuer = keycloakAuthServerUrl + "/realms/" + keycloakRealm;
|
||||
this.openidConfig = "{\n" +
|
||||
" \"issuer\": \"" + issuer + "\",\n" +
|
||||
" \"authorization_endpoint\": \"" + keycloakAuthServerUrl + "/realms/" + keycloakRealm +
|
||||
"/protocol/openid-connect/auth\",\n" +
|
||||
" \"token_endpoint\": \"" + keycloakAuthServerUrl + "/realms/" + keycloakRealm +
|
||||
"/protocol/openid-connect/token\",\n" +
|
||||
" \"token_introspection_endpoint\": \"" + keycloakAuthServerUrl + "/realms/" + keycloakRealm +
|
||||
"/protocol/openid-connect/token/introspect\",\n" +
|
||||
" \"userinfo_endpoint\": \"" + keycloakAuthServerUrl + "/realms/" + keycloakRealm +
|
||||
"/protocol/openid-connect/userinfo\",\n" +
|
||||
" \"end_session_endpoint\": \"" + keycloakAuthServerUrl + "/realms/" + keycloakRealm +
|
||||
"/protocol/openid-connect/logout\",\n" +
|
||||
" \"jwks_uri\": \"" + keycloakAuthServerUrl + "/realms/" + keycloakRealm +
|
||||
"/protocol/openid-connect/certs\",\n" +
|
||||
" \"check_session_iframe\": \"" + keycloakAuthServerUrl + "/realms/" + keycloakRealm +
|
||||
"/protocol/openid-connect/login-status-iframe.html\",\n" +
|
||||
" \"registration_endpoint\": \"" + keycloakAuthServerUrl + "/realms/" + keycloakRealm +
|
||||
"/clients-registrations/openid-connect\",\n" +
|
||||
" \"introspection_endpoint\": \"" + keycloakAuthServerUrl + "/realms/" + keycloakRealm +
|
||||
"/protocol/openid-connect/token/introspect\"\n" +
|
||||
"}";
|
||||
}
|
||||
|
||||
public void givenStub() {
|
||||
stubFor(get(urlEqualTo(String.format("/auth/realms/%s/.well-known/openid-configuration", keycloakRealm)))
|
||||
.willReturn(aResponse()
|
||||
.withHeader("Content-Type", "application/json")
|
||||
.withBody(openidConfig)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public String generateJwt(String... roles) {
|
||||
return jwtTokenBuilder.generateJwtWithRoles(issuer, roles);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package dev.vality.wachter.config;
|
||||
|
||||
import dev.vality.wachter.WachterApplication;
|
||||
import dev.vality.wachter.auth.utils.KeycloakOpenIdStub;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.cloud.contract.wiremock.AutoConfigureWireMock;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
|
||||
@SpringBootTest(
|
||||
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
|
||||
classes = {WachterApplication.class},
|
||||
properties = {
|
||||
"wiremock.server.baseUrl=http://localhost:${wiremock.server.port}",
|
||||
"keycloak.auth-server-url=${wiremock.server.baseUrl}/auth"})
|
||||
@AutoConfigureMockMvc
|
||||
@AutoConfigureWireMock(port = 0)
|
||||
@ExtendWith(SpringExtension.class)
|
||||
public abstract class AbstractKeycloakOpenIdAsWiremockConfig {
|
||||
|
||||
@Autowired
|
||||
private KeycloakOpenIdStub keycloakOpenIdStub;
|
||||
|
||||
@BeforeAll
|
||||
public static void setUp(@Autowired KeycloakOpenIdStub keycloakOpenIdStub) throws Exception {
|
||||
keycloakOpenIdStub.givenStub();
|
||||
}
|
||||
|
||||
protected String generateSimpleJwt() {
|
||||
return keycloakOpenIdStub.generateJwt();
|
||||
}
|
||||
}
|
@ -0,0 +1,140 @@
|
||||
package dev.vality.wachter.controller;
|
||||
|
||||
import dev.vality.bouncer.decisions.ArbiterSrv;
|
||||
import dev.vality.orgmanagement.AuthContextProviderSrv;
|
||||
import dev.vality.wachter.config.AbstractKeycloakOpenIdAsWiremockConfig;
|
||||
import dev.vality.wachter.exeptions.AuthorizationException;
|
||||
import dev.vality.wachter.exeptions.WachterException;
|
||||
import dev.vality.wachter.testutil.TMessageUtil;
|
||||
import lombok.SneakyThrows;
|
||||
import org.apache.http.client.HttpClient;
|
||||
import org.apache.thrift.protocol.TProtocolFactory;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
import static dev.vality.wachter.testutil.ContextUtil.*;
|
||||
import static java.util.UUID.randomUUID;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@TestPropertySource(properties = {"auth.enabled=true"})
|
||||
class ErrorControllerTest extends AbstractKeycloakOpenIdAsWiremockConfig {
|
||||
|
||||
@MockBean
|
||||
public AuthContextProviderSrv.Iface orgManagerClient;
|
||||
@MockBean
|
||||
public ArbiterSrv.Iface bouncerClient;
|
||||
@MockBean
|
||||
private HttpClient httpClient;
|
||||
|
||||
@Autowired
|
||||
private MockMvc mvc;
|
||||
|
||||
@Autowired
|
||||
private TProtocolFactory protocolFactory;
|
||||
|
||||
private AutoCloseable mocks;
|
||||
|
||||
private Object[] preparedMocks;
|
||||
|
||||
|
||||
@BeforeEach
|
||||
public void init() {
|
||||
mocks = MockitoAnnotations.openMocks(this);
|
||||
preparedMocks = new Object[]{httpClient, orgManagerClient, bouncerClient};
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void clean() throws Exception {
|
||||
verifyNoMoreInteractions(preparedMocks);
|
||||
mocks.close();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
@SneakyThrows
|
||||
void requestJudgementRestricted() {
|
||||
when(orgManagerClient.getUserContext(any())).thenReturn(createContextFragment());
|
||||
when(bouncerClient.judge(any(), any())).thenReturn(createJudgementRestricted());
|
||||
mvc.perform(post("/wachter")
|
||||
.header("Authorization", "Bearer " + generateSimpleJwt())
|
||||
.header("Service", "messages")
|
||||
.header("X-Request-ID", randomUUID())
|
||||
.header("X-Request-Deadline", Instant.now().plus(1, ChronoUnit.DAYS).toString())
|
||||
.content(TMessageUtil.createTMessage(protocolFactory)))
|
||||
.andDo(print())
|
||||
.andExpect(status().is4xxClientError())
|
||||
.andExpect(result -> assertTrue(result.getResolvedException() instanceof AuthorizationException))
|
||||
.andExpect(result -> assertEquals("No rights for darkside-the-best@mail.com to " +
|
||||
"perform methodName in service messages",
|
||||
result.getResolvedException().getMessage()));
|
||||
verify(orgManagerClient, times(1)).getUserContext(any());
|
||||
verify(bouncerClient, times(1)).judge(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@SneakyThrows
|
||||
void requestJudgementForbidden() {
|
||||
when(orgManagerClient.getUserContext(any())).thenReturn(createContextFragment());
|
||||
when(bouncerClient.judge(any(), any())).thenReturn(createJudgementForbidden());
|
||||
mvc.perform(post("/wachter")
|
||||
.header("Authorization", "Bearer " + generateSimpleJwt())
|
||||
.header("Service", "messages")
|
||||
.header("X-Request-ID", randomUUID())
|
||||
.header("X-Request-Deadline", Instant.now().plus(1, ChronoUnit.DAYS).toString())
|
||||
.content(TMessageUtil.createTMessage(protocolFactory)))
|
||||
.andDo(print())
|
||||
.andExpect(status().is4xxClientError())
|
||||
.andExpect(result -> assertTrue(result.getResolvedException() instanceof AuthorizationException))
|
||||
.andExpect(result -> assertEquals("No rights for darkside-the-best@mail.com to " +
|
||||
"perform methodName in service messages",
|
||||
result.getResolvedException().getMessage()));
|
||||
verify(orgManagerClient, times(1)).getUserContext(any());
|
||||
verify(bouncerClient, times(1)).judge(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@SneakyThrows
|
||||
void requestWithoutServiceHeader() {
|
||||
mvc.perform(post("/wachter")
|
||||
.header("Authorization", "Bearer " + generateSimpleJwt())
|
||||
.header("X-Request-ID", randomUUID())
|
||||
.header("X-Request-Deadline", Instant.now().plus(1, ChronoUnit.DAYS).toString())
|
||||
.content(TMessageUtil.createTMessage(protocolFactory)))
|
||||
.andDo(print())
|
||||
.andExpect(status().is4xxClientError())
|
||||
.andExpect(result -> assertTrue(result.getResolvedException() instanceof WachterException))
|
||||
.andExpect(result -> assertEquals("Header \"Service\" must be set",
|
||||
result.getResolvedException().getMessage()));
|
||||
}
|
||||
|
||||
@Test
|
||||
@SneakyThrows
|
||||
void requestWithWrongServiceHeader() {
|
||||
mvc.perform(post("/wachter")
|
||||
.header("Authorization", "Bearer " + generateSimpleJwt())
|
||||
.header("X-Request-ID", randomUUID())
|
||||
.header("Service", "wrong")
|
||||
.header("X-Request-Deadline", Instant.now().plus(1, ChronoUnit.DAYS).toString())
|
||||
.content(TMessageUtil.createTMessage(protocolFactory)))
|
||||
.andDo(print())
|
||||
.andExpect(status().is4xxClientError())
|
||||
.andExpect(result -> assertTrue(result.getResolvedException() instanceof WachterException))
|
||||
.andExpect(result -> assertEquals("Service \"wrong\" not found in configuration",
|
||||
result.getResolvedException().getMessage()));
|
||||
}
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
package dev.vality.wachter.controller;
|
||||
|
||||
import dev.vality.bouncer.decisions.ArbiterSrv;
|
||||
import dev.vality.orgmanagement.AuthContextProviderSrv;
|
||||
import dev.vality.wachter.config.AbstractKeycloakOpenIdAsWiremockConfig;
|
||||
import dev.vality.wachter.testutil.TMessageUtil;
|
||||
import lombok.SneakyThrows;
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.client.HttpClient;
|
||||
import org.apache.http.entity.StringEntity;
|
||||
import org.apache.thrift.protocol.TProtocolFactory;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
import static dev.vality.wachter.testutil.ContextUtil.createContextFragment;
|
||||
import static dev.vality.wachter.testutil.ContextUtil.createJudgementAllowed;
|
||||
import static java.util.UUID.randomUUID;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
class WachterControllerTest extends AbstractKeycloakOpenIdAsWiremockConfig {
|
||||
|
||||
@MockBean
|
||||
public AuthContextProviderSrv.Iface orgManagerClient;
|
||||
@MockBean
|
||||
public ArbiterSrv.Iface bouncerClient;
|
||||
@MockBean
|
||||
private HttpClient httpClient;
|
||||
@MockBean
|
||||
private HttpResponse httpResponse;
|
||||
|
||||
@Autowired
|
||||
private MockMvc mvc;
|
||||
|
||||
@Autowired
|
||||
private TProtocolFactory protocolFactory;
|
||||
|
||||
private AutoCloseable mocks;
|
||||
|
||||
private Object[] preparedMocks;
|
||||
|
||||
|
||||
@BeforeEach
|
||||
public void init() {
|
||||
mocks = MockitoAnnotations.openMocks(this);
|
||||
preparedMocks = new Object[]{httpClient, orgManagerClient, bouncerClient};
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void clean() throws Exception {
|
||||
verifyNoMoreInteractions(preparedMocks);
|
||||
mocks.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
@SneakyThrows
|
||||
void requestSuccess() {
|
||||
when(orgManagerClient.getUserContext(any())).thenReturn(createContextFragment());
|
||||
when(bouncerClient.judge(any(), any())).thenReturn(createJudgementAllowed());
|
||||
when(httpResponse.getEntity()).thenReturn(new StringEntity(""));
|
||||
when(httpClient.execute(any())).thenReturn(httpResponse);
|
||||
mvc.perform(post("/wachter")
|
||||
.header("Authorization", "Bearer " + generateSimpleJwt())
|
||||
.header("Service", "messages")
|
||||
.header("X-Request-ID", randomUUID())
|
||||
.header("X-Request-Deadline", Instant.now().plus(1, ChronoUnit.DAYS).toString())
|
||||
.content(TMessageUtil.createTMessage(protocolFactory)))
|
||||
.andDo(print())
|
||||
.andExpect(status().is2xxSuccessful());
|
||||
verify(orgManagerClient, times(1)).getUserContext(any());
|
||||
verify(bouncerClient, times(1)).judge(any(), any());
|
||||
verify(httpClient, times(1)).execute(any());
|
||||
}
|
||||
|
||||
}
|
61
src/test/java/dev/vality/wachter/testutil/ContextUtil.java
Normal file
61
src/test/java/dev/vality/wachter/testutil/ContextUtil.java
Normal file
@ -0,0 +1,61 @@
|
||||
package dev.vality.wachter.testutil;
|
||||
|
||||
import dev.vality.bouncer.ctx.ContextFragment;
|
||||
import dev.vality.bouncer.decisions.*;
|
||||
import dev.vality.geck.serializer.kit.mock.FieldHandler;
|
||||
import dev.vality.geck.serializer.kit.mock.MockMode;
|
||||
import dev.vality.geck.serializer.kit.mock.MockTBaseProcessor;
|
||||
import dev.vality.geck.serializer.kit.tbase.TBaseHandler;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import org.apache.thrift.TBase;
|
||||
import org.apache.thrift.TSerializer;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
|
||||
@UtilityClass
|
||||
public class ContextUtil {
|
||||
|
||||
private static final MockTBaseProcessor mockRequiredTBaseProcessor;
|
||||
|
||||
static {
|
||||
mockRequiredTBaseProcessor = new MockTBaseProcessor(MockMode.REQUIRED_ONLY, 15, 1);
|
||||
Map.Entry<FieldHandler, String[]> timeFields = Map.entry(
|
||||
structHandler -> structHandler.value(Instant.now().toString()),
|
||||
new String[]{"conversation_id", "messages", "status", "user_id", "email", "fullname",
|
||||
"held_until", "from_time", "to_time"}
|
||||
);
|
||||
mockRequiredTBaseProcessor.addFieldHandler(timeFields.getKey(), timeFields.getValue());
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public static <T extends TBase> T fillRequiredTBaseObject(T tbase, Class<T> type) {
|
||||
return ContextUtil.mockRequiredTBaseProcessor.process(tbase, new TBaseHandler<>(type));
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public static ContextFragment createContextFragment() {
|
||||
ContextFragment fragment = ContextUtil.fillRequiredTBaseObject(new ContextFragment(), ContextFragment.class);
|
||||
fragment.setContent(new TSerializer().serialize(new dev.vality.bouncer.context.v1.ContextFragment()));
|
||||
return fragment;
|
||||
}
|
||||
|
||||
public static Judgement createJudgementAllowed() {
|
||||
Resolution resolution = new Resolution();
|
||||
resolution.setAllowed(new ResolutionAllowed());
|
||||
return new Judgement().setResolution(resolution);
|
||||
}
|
||||
|
||||
public static Judgement createJudgementRestricted() {
|
||||
Resolution resolution = new Resolution();
|
||||
resolution.setRestricted(new ResolutionRestricted());
|
||||
return new Judgement().setResolution(resolution);
|
||||
}
|
||||
|
||||
public static Judgement createJudgementForbidden() {
|
||||
Resolution resolution = new Resolution();
|
||||
resolution.setForbidden(new ResolutionForbidden());
|
||||
return new Judgement().setResolution(resolution);
|
||||
}
|
||||
}
|
42
src/test/java/dev/vality/wachter/testutil/TMessageUtil.java
Normal file
42
src/test/java/dev/vality/wachter/testutil/TMessageUtil.java
Normal file
@ -0,0 +1,42 @@
|
||||
package dev.vality.wachter.testutil;
|
||||
|
||||
|
||||
import org.apache.thrift.TException;
|
||||
import org.apache.thrift.protocol.TMessage;
|
||||
import org.apache.thrift.protocol.TMessageType;
|
||||
import org.apache.thrift.protocol.TProtocol;
|
||||
import org.apache.thrift.protocol.TProtocolFactory;
|
||||
import org.apache.thrift.transport.TMemoryBuffer;
|
||||
import org.apache.thrift.transport.TTransportException;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public class TMessageUtil {
|
||||
|
||||
public static byte[] createTMessage(TProtocolFactory protocolFactory) throws TException {
|
||||
TMemoryBufferWithLength memoryBuffer = new TMemoryBufferWithLength(1024);
|
||||
TProtocol protocol = protocolFactory.getProtocol(memoryBuffer);
|
||||
protocol.writeMessageBegin(new TMessage("methodName", TMessageType.REPLY, 0));
|
||||
protocol.writeMessageEnd();
|
||||
return Arrays.copyOf(memoryBuffer.getArray(), memoryBuffer.length());
|
||||
}
|
||||
|
||||
public static class TMemoryBufferWithLength extends TMemoryBuffer {
|
||||
private int actualLength = 0;
|
||||
|
||||
public TMemoryBufferWithLength(int size) throws TTransportException {
|
||||
super(size);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] buf, int off, int len) {
|
||||
super.write(buf, off, len);
|
||||
actualLength += len;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int length() {
|
||||
return actualLength;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package dev.vality.wachter.util;
|
||||
|
||||
import dev.vality.wachter.testutil.TMessageUtil;
|
||||
import dev.vality.wachter.utils.MethodNameReader;
|
||||
import org.apache.thrift.TException;
|
||||
import org.apache.thrift.protocol.TProtocolFactory;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
@SpringBootTest
|
||||
class MethodNameReaderTest {
|
||||
|
||||
@Autowired
|
||||
private TProtocolFactory protocolFactory;
|
||||
|
||||
@Test
|
||||
void readMethodName() throws TException {
|
||||
byte[] message = TMessageUtil.createTMessage(protocolFactory);
|
||||
assertEquals("methodName", MethodNameReader.getMethodName(message));
|
||||
}
|
||||
|
||||
}
|
10
src/test/resources/logback-test.xml
Normal file
10
src/test/resources/logback-test.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
|
||||
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
|
||||
|
||||
<root level="warn">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
</root>
|
||||
<logger name="dev.vality.woody" level="ALL"/>
|
||||
</configuration>
|
Loading…
Reference in New Issue
Block a user