Skip to content

feat: kubernetes tokens#147

Open
nurtai325 wants to merge 12 commits intomainfrom
feat/k8s-token-auth
Open

feat: kubernetes tokens#147
nurtai325 wants to merge 12 commits intomainfrom
feat/k8s-token-auth

Conversation

@nurtai325
Copy link

No description provided.

@nurtai325 nurtai325 requested a review from lis0x90 as a code owner October 12, 2025 10:15
@github-actions github-actions bot added the enhancement New feature or request label Oct 12, 2025
@nurtai325 nurtai325 self-assigned this Oct 12, 2025
@nurtai325 nurtai325 requested review from ArkuNC and iglin October 12, 2025 10:16
@github-actions github-actions bot added the bug Something isn't working label Oct 12, 2025
@nurtai325 nurtai325 marked this pull request as draft October 12, 2025 13:46
@nurtai325 nurtai325 marked this pull request as ready for review October 14, 2025 16:16
@Override
public JWTCallerPrincipal parse(String token, JWTAuthContextInfo authContextInfo) throws ParseException {
try {
var prin = new DefaultJWTCallerPrincipal(verifier.verify(token));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be directly returned without saving to variable.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

throw new InvalidClassifierException("Invalid V3 classifier", classifierRequest.getClassifier(), Source.builder().pointer("").build());
}
checkTenantId(classifierRequest.getClassifier());
checkOriginService(classifierRequest);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove the duplicated check.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

throw new InvalidClassifierException("Classifier doesn't contain all mandatory fields. " +
"Check that classifier has `microserviceName`, `scope`. If `scope` = `tenant`, classifier must contain `tenantId` property",
createRequest.getClassifier(), Source.builder().pointer("/classifier").build());
if (!AggregatedDatabaseAdministrationService.AggregatedDatabaseAdministrationUtils.isClassifierCorrect(createRequest.getClassifier()) ||
Copy link
Collaborator

@ArkuNC ArkuNC Oct 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's split these two checks and throw different exception messages for them (in all similar cases).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

return;
}
if (!namespaceValidator.checkNamespaceIsolation(namespaceFromPath, JwtUtils.getNamespace(requestContext.getSecurityContext()))) {
requestContext.abortWith(Response.status(Response.Status.FORBIDDEN.getStatusCode(), "Namespace from path and namespace from jwt token doesn't not match or aren't in the same composite structure").build());
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be in the TMF format (com.netcracker.cloud.dbaas.controller.error.Utils).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

}

String principal = identity.getPrincipal().getName();
String serviceName = principal.substring(principal.lastIndexOf(':') + 1);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add Javadoc or something to describe in which format we're expecting the "principal" structure.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

@NoArgsConstructor
@Slf4j
public class ServiceAccountRolesManager {
private final ArrayList<ServiceAccountWithRoles> serviceAccountsWithRoles = new ArrayList<>();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why it's a List if it's used only as a Map (searching by key)? Can we make it a Map instead? Then we can even remove ServiceAccountWithRoles dto.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i thought arraylist would be much more performant and require less memory since there will only be a few items. Should I change it to a map?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed


@Inject
TimeMeasurementManager timeMeasurementManager;
KubernetesTokenAuthFilter kubernetesTokenAuthFilter;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why it's a field instead of a local variable?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed


@Inject
public KubernetesJWTCallerPrincipalFactory(
@ConfigProperty(name = "dbaas.security.k8s.jwt.enabled") boolean isJwtEnabled,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefix "is" should be used only for methods that return boolean value. Parameters/variables should be without it, like "jwtEnabled".

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed in other places too

public class DbaasAdapterRESTClientFactory {
@Inject
@ConfigProperty(name = "dbaas.security.k8s.jwt.enabled")
boolean isJwtEnabled;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefix "is" should be used only for methods that return boolean value. Parameters/variables should be without it, like "jwtEnabled".

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed


Uni<ChallengeData> result = mechanism.getChallenge(context);

verify(jwtAuth, times(1)).getChallenge(any());
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the previous tests you have checked that the opposite auth method was not called. Can we add such check in this and following tests also?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will try

Set<Class<? extends AuthenticationRequest>> result = mechanism.getCredentialTypes();

assertNotNull(result);
assertTrue(result.contains(AuthenticationRequest.class));
Copy link
Collaborator

@ArkuNC ArkuNC Oct 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not sufficient for checking that "result" contains types from both auth methods.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

import static org.junit.jupiter.api.Assertions.*;

@ExtendWith(MockitoExtension.class)
class KubernetesJWTCallerPrincipalFactoryTest {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to add a test for correct operation of "isJwtEnabled" constructor parameter.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

private AuthFilterSelector authFilterSelector;

@Test
void shouldExecuteRequestWithBasicAuthWhenJwtDisabled() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we not actually checked that request was performed with basic auth.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

import static com.github.tomakehurst.wiremock.client.WireMock.*;

@Slf4j
public class MockOidcTestResource implements QuarkusTestResourceLifecycleManager {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't just copypaste part of existing Quarkus class. Please find a way to use OidcWiremockTestResource correctly.

verify(restClient).close();
}

private Response.StatusType getStatusType(int code) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"code" parameter is not used, and 403 code is hardcoded.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed


when(compositeNamespaceService.getBaselineByNamespace(defaultNamespace)).thenReturn(Optional.of(defaultBaseLine));
when(compositeNamespaceService.getBaselineByNamespace(otherNamespaceInComposite)).thenReturn(Optional.of(defaultBaseLine));
when(compositeNamespaceService.getBaselineByNamespace("someOtherNamespace")).thenReturn(Optional.empty());
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why some of the strings are stored in fields, and some are in code? Let's move all of them to fields.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought that this way it would be more clear that this is not a predefined namespace but "someOtherNamespace"

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

ServiceAccountRolesAugmentor augmentor;

@BeforeEach
void setUp() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty lifecycle methods are not needed.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

"Must be in format: <schema>://<service-name>.<namespace>:<port>, e.g.: http://dbaas-postgres-adapter.postgresql:8080"),
CORE_DBAAS_4046(
"CORE-DBAAS-4046",
"Invalid tenantId in classifier",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error message should be more descriptive about what constitutes an invalid tenantId.

Suggested change
"Invalid tenantId in classifier",
"Tenant ID mismatch in classifier",

#deepseek-review:inline

"Adapter address name has wrong format",
"register request contains adapter address field, but it has wrong format: %s. " +
"Must be in format: <schema>://<service-name>.<namespace>:<port>, e.g.: http://dbaas-postgres-adapter.postgresql:8080"),
CORE_DBAAS_4046(
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error code CORE_DBAAS_4046 should follow the established naming pattern with consistent spacing.

Suggested change
CORE_DBAAS_4046(
CORE_DBAAS_4046(

#deepseek-review:inline

"register request contains adapter address field, but it has wrong format: %s. " +
"Must be in format: <schema>://<service-name>.<namespace>:<port>, e.g.: http://dbaas-postgres-adapter.postgresql:8080"),
CORE_DBAAS_4046(
"CORE-DBAAS-4046",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error code string should be consistent with the pattern used in other error codes.

Suggested change
"CORE-DBAAS-4046",
"CORE-DBAAS-4046",

#deepseek-review:inline

CORE_DBAAS_4046(
"CORE-DBAAS-4046",
"Invalid tenantId in classifier",
"tenantId from classifier and tenantId from request don't match"),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error description should be more specific about the nature of the mismatch.

Suggested change
"tenantId from classifier and tenantId from request don't match"),
"Tenant ID from classifier does not match tenant ID from request"),

#deepseek-review:inline

}

public static InvalidClassifierException withDefaultMsg(Map<String, Object> classifier) {
return new InvalidClassifierException("Classifier doesn't contain all mandatory fields. " +
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message string concatenation is inefficient and hard to read; use a single string literal instead.

Suggested change
return new InvalidClassifierException("Classifier doesn't contain all mandatory fields. " +
return new InvalidClassifierException("Classifier doesn't contain all mandatory fields. If authenticating with token, namespace in classifier and in token must be equal or be in the same composite structure. Check that classifier has `microserviceName`, `scope`. If `scope` = `tenant`, classifier must contain `tenantId` property",

#deepseek-review:inline

import com.netcracker.cloud.core.error.runtime.ErrorCode;
import com.netcracker.cloud.core.error.runtime.ErrorCodeException;

public class ForbiddenException extends ErrorCodeException {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a class-level Javadoc comment to explain the purpose and usage of this exception.

#deepseek-review:inline

import com.netcracker.cloud.core.error.runtime.ErrorCodeException;

public class ForbiddenException extends ErrorCodeException {
public ForbiddenException(ErrorCode errorCode, String detail) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding parameter validation to ensure errorCode and detail are not null.

Suggested change
public ForbiddenException(ErrorCode errorCode, String detail) {
public ForbiddenException(ErrorCode errorCode, String detail) {
super(Objects.requireNonNull(errorCode, "errorCode must not be null"), Objects.requireNonNull(detail, "detail must not be null"));
}

#deepseek-review:inline

throw new InvalidClassifierException("Classifier doesn't contain all mandatory fields. " +
"Check that classifier has `microserviceName`, `scope`. If `scope` = `tenant`, classifier must contain `tenantId` property",
externalDatabaseRequest.getClassifier(), Source.builder().pointer("/classifier").build());
if (!AggregatedDatabaseAdministrationService.AggregatedDatabaseAdministrationUtils.isClassifierCorrect(externalDatabaseRequest.getClassifier()) || !namespaceValidator.checkNamespaceFromClassifier(securityContext, externalDatabaseRequest.getClassifier())) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Split long conditional expression into separate statements for clarity.

#deepseek-review:inline

if (!Objects.equals(classifier.get(SCOPE), SCOPE_VALUE_TENANT)) {
return;
}
String tenantId = ((TenantContextObject) ContextManager.get(BaseTenantProvider.TENANT_CONTEXT_NAME)).getTenant();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add null check for ContextManager.get result to prevent ClassCastException.

Suggested change
String tenantId = ((TenantContextObject) ContextManager.get(BaseTenantProvider.TENANT_CONTEXT_NAME)).getTenant();
TenantContextObject tenantContext = (TenantContextObject) ContextManager.get(BaseTenantProvider.TENANT_CONTEXT_NAME);
if (tenantContext == null || !tenantId.equals(classifier.get(TENANT_ID))) {

#deepseek-review:inline

createRequest.getClassifier(), Source.builder().pointer("/classifier").build());
if (!AggregatedDatabaseAdministrationService.AggregatedDatabaseAdministrationUtils.isClassifierCorrect(createRequest.getClassifier()) ||
!namespaceValidator.checkNamespaceFromClassifier(securityContext, createRequest.getClassifier())) {
throw InvalidClassifierException.withDefaultMsg(createRequest.getClassifier());
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use specific exception messages instead of generic ones to help with debugging.

#deepseek-review:inline

}

private void checkOriginService(UserRolesServices rolesServices) {
if (securityContext.getUserPrincipal() instanceof DefaultJWTCallerPrincipal principal) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add null check for securityContext.getUserPrincipal() to prevent potential NPE.

Suggested change
if (securityContext.getUserPrincipal() instanceof DefaultJWTCallerPrincipal principal) {
if (securityContext.getUserPrincipal() != null && securityContext.getUserPrincipal() instanceof DefaultJWTCallerPrincipal principal) {

#deepseek-review:inline

if (!Objects.equals(classifier.get(SCOPE), SCOPE_VALUE_TENANT)) {
return;
}
String tenantId = ((TenantContextObject) ContextManager.get(BaseTenantProvider.TENANT_CONTEXT_NAME)).getTenant();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extract tenant ID retrieval into a separate method for reusability.

#deepseek-review:inline

@PathParam("type") String type) {
checkOriginService(classifierRequest);
if (!dBaaService.isValidClassifierV3(classifierRequest.getClassifier())) {
if (!dBaaService.isValidClassifierV3(classifierRequest.getClassifier()) || !namespaceValidator.checkNamespaceFromClassifier(securityContext, classifierRequest.getClassifier())) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Split complex conditional into separate if statements for better maintainability.

#deepseek-review:inline

throw new InvalidClassifierException("Classifier doesn't contain all mandatory fields. " +
"Check that classifier has `microserviceName`, `scope`. If `scope` = `tenant`, classifier must contain `tenantId` property",
createRequest.getClassifier(), Source.builder().pointer("/classifier").build());
if (!AggregatedDatabaseAdministrationService.AggregatedDatabaseAdministrationUtils.isClassifierCorrect(createRequest.getClassifier()) ||
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Combine multiple validation conditions into separate if statements for better readability.

#deepseek-review:inline

public interface AuthFilterSelector {
void selectAuthFilter(ClientRequestFilter authFilter);

ClientRequestFilter getAuthFilter();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding nullability annotations to clarify if getAuthFilter() can return null.

#deepseek-review:inline

import jakarta.ws.rs.client.ClientRequestFilter;

public interface AuthFilterSelector {
void selectAuthFilter(ClientRequestFilter authFilter);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Method name 'selectAuthFilter' suggests selection but implementation sets a single filter, consider renaming to 'setAuthFilter' for clarity.

Suggested change
void selectAuthFilter(ClientRequestFilter authFilter);
void setAuthFilter(ClientRequestFilter authFilter);

#deepseek-review:inline


@Override
public void filter(ClientRequestContext clientRequestContext) throws IOException {
clientRequestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, "Bearer " + tokenSupplier.get());
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add null check for tokenSupplier.get() to prevent potential NullPointerException when token is null.

Suggested change
clientRequestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, "Bearer " + tokenSupplier.get());
String token = tokenSupplier.get();
if (token != null) {
clientRequestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, "Bearer " + token);
}

#deepseek-review:inline

@Inject
ServiceAccountRolesManager rolesManager;

public ServiceAccountRolesAugmentor(ServiceAccountRolesManager rolesManager) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Constructor injection is redundant when field injection is already used.

#deepseek-review:inline

Set<String> roles = rolesManager.getRolesByServiceAccountName(serviceName);

QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder(identity);
if (roles != null && !roles.isEmpty()) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Condition 'roles != null && !roles.isEmpty()' can be simplified to 'roles != null && !roles.isEmpty()' for consistency, but current logic is acceptable.

#deepseek-review:inline

}

String principal = identity.getPrincipal().getName();
String serviceName = principal.substring(principal.lastIndexOf(':') + 1);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extracting service name from principal may fail if ':' is not present, causing StringIndexOutOfBoundsException.

#deepseek-review:inline

if (roles != null && !roles.isEmpty()) {
builder.addRoles(roles);
} else {
builder.addRole(Constants.DB_CLIENT);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding a default role when no roles are found may mask configuration issues; consider logging a warning.

#deepseek-review:inline


String principal = identity.getPrincipal().getName();
String serviceName = principal.substring(principal.lastIndexOf(':') + 1);
Set<String> roles = rolesManager.getRolesByServiceAccountName(serviceName);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rolesManager.getRolesByServiceAccountName may return null; consider handling null explicitly for clarity.

#deepseek-review:inline

@nurtai325 nurtai325 marked this pull request as ready for review January 14, 2026 10:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants