Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .github/settings.yml

This file was deleted.

10 changes: 0 additions & 10 deletions .github/workflows/basic-linters.yml

This file was deleted.

49 changes: 22 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,22 @@
# wachter

Сервис предназначен для авторизации и проксирования вызовов от [control-center](https://github.com/valitydev/control-center).

## Описание работы сервиса

1. Wachter получает от [control-center](https://github.com/valitydev/control-center) запрос на проведение операции,
содержащий токен и имя сервиса, в который необходимо спроксировать запрос. Имя сервиса получает из header "Service".
2. Из сообщения запроса wachter получает
[имя метода](https://github.com/valitydev/wachter/blob/master/src/main/java/dev/vality/wachter/service/MethodNameReaderService.java)
3. В [KeycloakService](https://github.com/valitydev/wachter/blob/master/src/main/java/dev/vality/wachter/service/KeycloakService.java)
wachter получает AccessToken.
4. По имени сервиса из header wachter
[маппит](https://github.com/valitydev/wachter/blob/master/src/main/java/dev/vality/wachter/mapper/ServiceMapper.java)
url, на который необходимо спроксировать запрос.
5. Далее сервис проверяет возможность [авторизации](https://github.com/valitydev/wachter/blob/master/src/main/java/dev/vality/wachter/security/AccessService.java)
пользователя, сравнивая полученные названия сервиса и метода от [control-center](https://github.com/valitydev/control-center)
с теми, что находятся в JWT токене. Доступ может быть разрешен как [ко всему сервису](https://github.com/valitydev/wachter/blob/master/src/main/java/dev/vality/wachter/security/RoleAccessService.java#L22),
так и только к [отдельному методу](https://github.com/valitydev/wachter/blob/master/src/main/java/dev/vality/wachter/security/RoleAccessService.java#L22) сервиса.
6. Если доступ разрешен, сервис [отправляет запрос](https://github.com/valitydev/wachter/blob/master/src/main/java/dev/vality/wachter/client/WachterClient.java)
на ранее смаппленный урл.
7. Полученный ответ [возвращает](https://github.com/valitydev/wachter/blob/master/src/main/java/dev/vality/wachter/controller/WachterController.java) control-center.

Схема работы сервиса:

![diagram-wachter](doc/diagram-wachter.svg)

# wachter

Сервис авторизации и прозрачного проксирования запросов от внешних систем к внутренним доменным сервисам. Представляет из себя HTTP пайплайн для Thrift вызовов с поддержкой заголовков woody и метаданных авторизации

## Основной поток

1. **Фильтрация входящего запроса.** `WoodyTracingFilter` нормализует заголовки `x-woody-*`/`woody.*`, восстанавливает `TraceContext` и создаёт серверный OpenTelemetry span с гарантированным `traceparent`.
2. **Авторизация.** `WachterService` считывает фактический метод из thrift-пакета, извлекает JWT из Spring Security, проверяет права пользователя через `AccessService`/`RoleAccessService`.
3. **Определение целевого сервиса.** `ServiceMapper` выбирает URL по заголовку `Service`.
4. **Формирование запроса.** `WachterRequestFactory` собирает исходные заголовки, накладывает нормализованные Woody-заголовки и значения из текущего `TraceContext`, дополняет идентификационные поля из JWT.
5. **Отправка и получение ответа.** `WachterClient` использует `RestClient` (JDK HTTP) для вызова доменного сервиса, возвращая `WachterClientResponse` со статусом, заголовками и телом.
6. **Ответ потребителю.** `WachterController` проверяет дедлайн, передаёт данные в `WachterService` и возвращает клиенту неизменённые статус, заголовки и тело от upstream.

## Особенности

- Поддержка двух семейств Woody-заголовков (новые `woody.*` и наследуемые `x-woody-*`).
- Автоматическая генерация и распространение OpenTelemetry `traceparent` при отсутствии входящего заголовка.
- Выделенный `JwtTokenDetailsExtractor` для повторного использования данных токена.
- Тестовый контур покрывает композицию фильтра, клиента и контроллера, включая WireMock-интеграцию.

Схема взаимодействий остаётся доступной в [doc/diagram-wachter.svg](doc/diagram-wachter.svg).

48 changes: 48 additions & 0 deletions agents.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Agents Guide

This document helps automation agents work effectively on the **Wachter** service.

## Project Snapshot
- **Purpose:** authorize requests from Control Center and transparently proxy them to domain services.
- **Stack:** Java 21, Spring Boot 3, RestClient (JDK HTTP), OpenTelemetry (OTLP/HTTP), Woody tracing library, JWT via Spring Security.
- **Tracing Flow:** `WoodyTracingFilter` normalizes Woody headers, hydrates Woody trace context, starts an OpenTelemetry span, and stores normalized headers in the request.
- **Proxy Flow:** `WachterService` performs access checks, `WachterClient` (using `RestClient`) forwards requests, and `WachterController` returns upstream status/headers/body unchanged.

## Key Components
- `dev.vality.wachter.config` – Spring configuration; `ApplicationConfig`, `WebConfig`, `OtelConfig`.
- `dev.vality.wachter.config.tracing` – tracing helpers (`WoodyHeadersNormalizer`, `WoodyTraceContextApplier`, `WoodyTelemetrySupport`, `WoodyTracingFilter`).
- `dev.vality.wachter.client` – outbound proxy pieces (`WachterClient`, `WachterRequestFactory`, `WachterClientResponse`).
- `dev.vality.wachter.security` – access control (`AccessService`, `RoleAccessService`, `JwtTokenDetailsExtractor`).
- `dev.vality.wachter.service` – application services (`WachterService`, `MethodNameReaderService`).
- `dev.vality.wachter.controller` – REST controllers (`WachterController`, error handling tests).
- `dev.vality.wachter.constants` – header/attribute constants.

## Tests & Verification
- **Unit tests:** `mvn test` (runs JUnit + WireMock integration suite).
- **Key suites:** `WachterClient*Test`, `WebConfigTest`, controller tests, `WachterIntegrationTest` (WireMock-based end-to-end proxy verification).
- Ensure new features include coverage across: header normalization, trace context propagation, authorization checks, proxy behaviour.

## Conventions & Practices
- Maintain dual Woody headers (`woody.*` + `x-woody-*`).
- Preserve upstream responses exactly (status, headers, body).
- Use `JwtTokenDetailsExtractor` for JWT-derived values; avoid duplicating claim parsing.
- When touching tracing, ensure OpenTelemetry span attributes (`HTTP_*`, `traceparent`) remain intact.
- Follow existing Checkstyle conventions: `final` for immutable locals, concise logging via SLF4J, minimal inline comments.
- Update documentation only when requested.

## Common Tasks
1. **Modify tracing behaviour:** inspect `config/tracing` classes; update tests in `WebConfigTest` and `WachterIntegrationTest`.
2. **Adjust outbound proxying:** change `WachterRequestFactory`/`WachterClient`; add/update tests in `client` package and integration suite.
3. **Authorization changes:** update `AccessService`, `RoleAccessService`; extend security tests accordingly.

## Useful Commands
- Run full suite: `mvn test`
- Format/imports: rely on IDE (no automatic formatter configured).
- Generate coverage (optional): `mvn test -Pcoverage` (if profile exists; verify before use).

## Gotchas
- `WachterController` enforces deadline checks before proxying; always keep `DeadlineUtil` behaviour in mind.
- `WoodyHeadersNormalizer` merges JWT metadata and deadlines with priority rules—changing behaviour requires revisiting corresponding tests.
- Integration tests spin up WireMock; avoid port conflicts by keeping default configuration.

Stay aligned with the README and trace plans when planning new work.
70 changes: 16 additions & 54 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
<parent>
<groupId>dev.vality</groupId>
<artifactId>service-parent-pom</artifactId>
<version>3.1.7</version>
<version>3.1.9</version>
</parent>

<artifactId>wachter</artifactId>
<version>1.0-SNAPSHOT</version>
<version>1.0.0</version>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
Expand All @@ -25,30 +25,20 @@
<dependencies>
<!--vality-->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
<groupId>dev.vality.woody</groupId>
<artifactId>woody-api</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<dependency>
<groupId>dev.vality</groupId>
<artifactId>shared-resources</artifactId>
<version>${shared-resources.version}</version>
</dependency>
<dependency>
<groupId>dev.vality</groupId>
<artifactId>bouncer-proto</artifactId>
<version>1.57-31866c3</version>
<groupId>dev.vality.woody</groupId>
<artifactId>woody-thrift</artifactId>
</dependency>
<dependency>
<groupId>dev.vality.geck</groupId>
<artifactId>serializer</artifactId>
</dependency>
<dependency>
<groupId>dev.vality</groupId>
<artifactId>damsel</artifactId>
<artifactId>woody-http-bridge</artifactId>
</dependency>

<!--spring-->
Expand Down Expand Up @@ -97,15 +87,9 @@
</dependency>

<!--third party-->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.14</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>jakarta.servlet</groupId>
Expand All @@ -124,12 +108,6 @@
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
<version>1.79</version>
<scope>test</scope>
</dependency>

<!--test-->
<dependency>
Expand All @@ -156,9 +134,15 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-wiremock</artifactId>
<version>4.1.0</version>
<groupId>org.wiremock.integrations</groupId>
<artifactId>wiremock-spring-boot</artifactId>
<version>3.10.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
<version>1.79</version>
<scope>test</scope>
</dependency>
</dependencies>
Expand Down Expand Up @@ -216,28 +200,6 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.cyclonedx</groupId>
<artifactId>cyclonedx-maven-plugin</artifactId>
<executions>
<execution>
<phase>generate-resources</phase>
<goals>
<goal>makeAggregateBom</goal>
</goals>
<configuration>
<projectType>application</projectType>
<outputDirectory>${project.build.directory}</outputDirectory>
<outputFormat>json</outputFormat>
<outputName>bom</outputName>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
95 changes: 95 additions & 0 deletions src/main/java/dev/vality/wachter/client/ProxyHeadersExtractor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package dev.vality.wachter.client;

import dev.vality.woody.http.bridge.tracing.TraceHeadersConstants;
import jakarta.servlet.http.HttpServletRequest;
import lombok.experimental.UtilityClass;
import org.springframework.http.HttpHeaders;
import org.springframework.util.StringUtils;

import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@UtilityClass
public class ProxyHeadersExtractor {

private static final Set<String> HOP_BY_HOP_HEADERS = Stream.of(
HttpHeaders.CONNECTION,
HttpHeaders.PROXY_AUTHENTICATE,
HttpHeaders.PROXY_AUTHORIZATION,
HttpHeaders.TE,
HttpHeaders.TRAILER,
HttpHeaders.TRANSFER_ENCODING,
HttpHeaders.UPGRADE,
"keep-alive",
"proxy-connection"
).map(header -> header.toLowerCase(Locale.ROOT)).collect(Collectors.toSet());

private static final Set<String> EXCLUDED_HEADERS = Stream.of(
HttpHeaders.AUTHORIZATION,
HttpHeaders.CONTENT_LENGTH,
HttpHeaders.HOST,
HttpHeaders.COOKIE,
HttpHeaders.SET_COOKIE,
HttpHeaders.SET_COOKIE2,
HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD,
HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS,
HttpHeaders.CACHE_CONTROL,
HttpHeaders.PRAGMA,
HttpHeaders.ORIGIN,
HttpHeaders.REFERER,
"dnt",
"priority",
"service",
TraceHeadersConstants.OTEL_TRACE_PARENT,
TraceHeadersConstants.OTEL_TRACE_STATE,
TraceHeadersConstants.ExternalHeaders.X_REQUEST_ID,
TraceHeadersConstants.ExternalHeaders.X_REQUEST_DEADLINE,
TraceHeadersConstants.ExternalHeaders.X_INVOICE_ID
).map(header -> header.toLowerCase(Locale.ROOT)).collect(Collectors.toSet());

private static final List<String> EXCLUDED_PREFIXES = List.of(
"cf-",
"cdn-",
"sec-",
TraceHeadersConstants.WOODY_PREFIX,
TraceHeadersConstants.ExternalHeaders.X_WOODY_PREFIX
);

public HttpHeaders extractHeaders(HttpServletRequest request) {
var collected = new HttpHeaders();
var headerNames = request.getHeaderNames();
if (headerNames == null) {
return collected;
}
while (headerNames.hasMoreElements()) {
var headerName = headerNames.nextElement();
var lowerCaseName = headerName.toLowerCase(Locale.ROOT);
if (shouldSkip(lowerCaseName)) {
continue;
}
var values = request.getHeaders(headerName);
while (values.hasMoreElements()) {
var value = values.nextElement();
if (StringUtils.hasText(value)) {
collected.add(headerName, value);
}
}
}
return collected;
}

private boolean shouldSkip(String lowerCaseHeaderName) {
if (HOP_BY_HOP_HEADERS.contains(lowerCaseHeaderName) || EXCLUDED_HEADERS.contains(lowerCaseHeaderName)) {
return true;
}
for (var prefix : EXCLUDED_PREFIXES) {
if (lowerCaseHeaderName.startsWith(prefix)) {
return true;
}
}
return false;
}
}
Loading