Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
23f2560
create kubernetes client package
nurtai325 Aug 20, 2025
bce86e4
add golang-jwt/jwt package
nurtai325 Aug 20, 2025
bcbfdf0
create new kubernetes/oidc package for verifying sa tokens
nurtai325 Aug 20, 2025
f4688ef
use go-oidc instead of custom logic
nurtai325 Aug 20, 2025
3a96bfc
instead of provider struct use verifier interface
nurtai325 Aug 21, 2025
0140521
tests for token source and verifier
nurtai325 Aug 21, 2025
1ac1244
create oidc verifier and pass to apis. new IsAccessGrantedWithToken func
nurtai325 Aug 21, 2025
40eb416
k8s token authentication added to SecurityMiddleware
nurtai325 Aug 21, 2025
8fab3be
fix build errors with new SecurityMiddleware
nurtai325 Aug 21, 2025
310865c
regenerate mock auth
nurtai325 Aug 22, 2025
dce2d18
return auth error when verifing k8s token
nurtai325 Aug 22, 2025
695104e
add testing bearer auth on SecurityMiddleware test
nurtai325 Aug 22, 2025
fb12881
move new secure http client from oidc to utils package
nurtai325 Aug 25, 2025
43cb92c
remove from config oidc.secure and infer that from issuer
nurtai325 Aug 25, 2025
0cb488c
change Dockerfile build image from go 22 to go 23
nurtai325 Aug 25, 2025
457d0af
feat: verifier infers issuer security by looking at the host
nurtai325 Aug 25, 2025
4559cd0
fileTokenSource now watches for file system changes and refreshes token
nurtai325 Aug 25, 2025
72c4ff3
test new file token source that watches file system
nurtai325 Aug 25, 2025
f116fd9
refactor verifier and token source
nurtai325 Aug 25, 2025
4500a41
fix token_source tests. use select with a for loop
nurtai325 Aug 25, 2025
770195b
fix utils.NewCertPool. use decoded pem data instead of directly passing
nurtai325 Aug 25, 2025
4c51c9f
comment out oidc setup in server.go
nurtai325 Aug 26, 2025
83d3492
test for service.auth.IsAccessGrantedWithToken
nurtai325 Aug 26, 2025
f08cfb5
created tests for the SecureHttpClient
nurtai325 Aug 26, 2025
f0f50db
uncomment oidc creation in server.go
nurtai325 Aug 26, 2025
9705ea7
when parsing Authorization header switch using strings.Split to using
nurtai325 Aug 26, 2025
f6d519e
instead of passing oidc.Verifier to
nurtai325 Aug 26, 2025
ebb226b
rename service.AuthService.IsAccessGranted to IsAccessGrantedWithBasic
nurtai325 Aug 26, 2025
7063834
oidc: remove using ca.crt from kubernetes and always use token. infer
nurtai325 Aug 26, 2025
f87b0f6
oidc: get issuer from the service account token mounted
nurtai325 Aug 26, 2025
a3d5257
oidc: remove function for inferring whether to use secure client or not
nurtai325 Aug 27, 2025
b22b36f
auth: refactor SecurityMiddleware with regexes
nurtai325 Aug 27, 2025
b8bda8a
server.go: comment out creating oidc verifier
nurtai325 Aug 27, 2025
6ece949
server.go: uncomment creating oidc verifier
nurtai325 Aug 27, 2025
42a036a
oidc: place tokendir to config so we can override it
nurtai325 Aug 27, 2025
8f4a008
oidc: remove kubernetes.oidc.tokendir config value
nurtai325 Aug 27, 2025
2f595a6
oidc: fix integration tests by making a mock oidc server test contain…
nurtai325 Aug 27, 2025
7850c08
oidc: when inferring issuer from kubernetes token accept tokens with
nurtai325 Aug 27, 2025
fd80b6a
oidc: add default constructors to kubernetes.oidc.fileTokenSource and
nurtai325 Aug 27, 2025
712a9fb
oidc: for integration testing make a testcontainer with oidc-server
nurtai325 Aug 27, 2025
595e0b1
oidc: in integration tests use dex oidc server instead of custom server
nurtai325 Aug 27, 2025
9452140
oidc: rename package logger
nurtai325 Aug 28, 2025
2801bc6
update Dockerfile base image version to 1.2.0
nurtai325 Sep 4, 2025
b7987a7
Merge branch 'main' into feature/k8s-token-authentication
nurtai325 Sep 4, 2025
169f664
fix: go.mod
nurtai325 Sep 4, 2025
61b368b
Merge branch 'main' into feature/k8s-token-authentication
nurtai325 Sep 6, 2025
877b836
fix: use tokensource and tokenverifier packages from core-lib-go
nurtai325 Oct 14, 2025
6f92c54
Merge branch 'main' into feature/k8s-token-authentication
nurtai325 Oct 14, 2025
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
6 changes: 6 additions & 0 deletions maas-integration-tests/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,12 @@
<version>3.17.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>wf.garnier</groupId>
<artifactId>testcontainers-dex</artifactId>
<version>3.2.0</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,17 @@
import org.testcontainers.containers.*;
import org.testcontainers.images.builder.ImageFromDockerfile;
import org.testcontainers.utility.DockerImageName;
import org.testcontainers.utility.MountableFile;
import wf.garnier.testcontainers.dexidp.DexContainer;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.PosixFilePermission;
import java.util.Base64;
import java.util.Map;
import java.util.Set;

import static org.qubership.it.maas.MaasITHelper.TEST_NAMESPACE;
import static org.junit.jupiter.api.Assertions.assertTrue;
Expand All @@ -30,6 +36,39 @@ public abstract class AbstractMaasWithInitsIT extends AbstractMaasIT {

protected static final PostgreSQLContainer<?> POSTGRES_CONTAINER = new PostgreSQLContainer<>(DockerImageName.parse("postgres:16.2")).withNetwork(TEST_NETWORK);

protected static final DexContainer OIDC_SERVER_CONTAINER = new DexContainer(DexContainer.DEFAULT_IMAGE_NAME.withTag(DexContainer.DEFAULT_TAG))
.withNetwork(TEST_NETWORK)
.withNetworkAliases("oidc-server")
.withExposedPorts(5556, 5557)
.withCopyFileToContainer(
MountableFile.forClasspathResource("dex-config.yaml"),
"/etc/dex/config.yaml")
.withCommand("dex", "serve", "/etc/dex/config.yaml");

private static final Path oidcTokenTempFile;

static {
try {
oidcTokenTempFile = Files.createTempFile("", "");
Files.writeString(oidcTokenTempFile, Utils.getNewJwt("http://%s:%d/dex".formatted(OIDC_SERVER_CONTAINER.getNetworkAliases().getLast(), 5556)));
try {
Set<PosixFilePermission> permissions = Set.of(
PosixFilePermission.OWNER_READ,
PosixFilePermission.OWNER_WRITE,
PosixFilePermission.GROUP_READ,
PosixFilePermission.OTHERS_READ
);
Files.setPosixFilePermissions(oidcTokenTempFile, permissions);
} catch (UnsupportedOperationException e) {
oidcTokenTempFile.toFile().setReadable(true, true);
oidcTokenTempFile.toFile().setWritable(true, true);
oidcTokenTempFile.toFile().setReadable(true, false);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}

protected static final GenericContainer<?> MAAS_CONTAINER = new GenericContainer<>(
new ImageFromDockerfile()
.withFileFromPath(".", Paths.get("../maas"))
Expand All @@ -44,6 +83,8 @@ public abstract class AbstractMaasWithInitsIT extends AbstractMaasIT {
)
.withNetwork(TEST_NETWORK)
.withExposedPorts(8080)
.withCopyFileToContainer(MountableFile.forHostPath(oidcTokenTempFile.toAbsolutePath()), "/var/run/secrets/kubernetes.io/serviceaccount/token")
Copy link
Contributor

Choose a reason for hiding this comment

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

where are the actual tests that use new auth ?

Copy link
Author

Choose a reason for hiding this comment

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

this is added because when application starts it looks for token and makes OIDC discovery. and this is added to prevent app from crashing at start in tests

.dependsOn(OIDC_SERVER_CONTAINER)
.dependsOn(POSTGRES_CONTAINER);

static {
Expand All @@ -55,6 +96,8 @@ public abstract class AbstractMaasWithInitsIT extends AbstractMaasIT {
RABBITMQ_CONTAINER_1.start();
RABBITMQ_CONTAINER_2.start();

OIDC_SERVER_CONTAINER.start();

MAAS_CONTAINER.start();

assertTrue(createManagersAccount());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import lombok.extern.slf4j.Slf4j;

import java.util.Base64;

@Slf4j
public class Utils {
@FunctionalInterface
Expand All @@ -25,4 +27,21 @@ public static void retry(int maxAttempts, OmnivoreRunnable f) throws Exception {
}
}
}

public static String getNewJwt(String issuer) {
String headerJson = "{\"alg\":\"none\",\"typ\":\"JWT\"}";
String encodedHeader = base64UrlEncode(headerJson);

String payloadJson = String.format("{\"iss\":\"%s\"}", issuer);
String encodedPayload = base64UrlEncode(payloadJson);

String signature = "";
return String.format("%s.%s.%s", encodedHeader, encodedPayload, signature);
}

private static String base64UrlEncode(String data) {
return Base64.getUrlEncoder()
.withoutPadding()
.encodeToString(data.getBytes());
}
}
18 changes: 18 additions & 0 deletions maas-integration-tests/src/test/resources/dex-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
issuer: http://oidc-server:5556/dex

storage:
type: sqlite3
Copy link
Contributor

Choose a reason for hiding this comment

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

do we need sqlite3 for tests?


web:
http: 0.0.0.0:5556

telemetry:
http: 0.0.0.0:5558
Copy link
Contributor

Choose a reason for hiding this comment

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

is this port used?


grpc:
addr: 0.0.0.0:5557
Copy link
Contributor

Choose a reason for hiding this comment

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

is this port used?


connectors:
Copy link
Contributor

Choose a reason for hiding this comment

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

what do we mock?

- type: mockCallback
id: mock
name: Example
6 changes: 5 additions & 1 deletion maas/maas-service/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ db:
cipher:
key: thisis32bitlongpassphraseimusing

kubernetes:
oidc:
audience: maas


# DR mode defaults
execution.mode: active
Expand All @@ -40,4 +44,4 @@ kafka.client.timeout: 10s
health.check.interval: 5s

# Migrate cr
fallback.cr.apiversion: ""
fallback.cr.apiversion: ""
2 changes: 1 addition & 1 deletion maas/maas-service/controller/account_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ func (a *AccountController) SaveManagerAccount(fiberCtx *fiber.Ctx) error {
}

log.InfoC(ctx, "Received request on create one more manage account")
if _, err := a.service.IsAccessGranted(ctx, username, password, model.ManagerAccountNamespace, []model.RoleName{model.ManagerRole}); err != nil {
if _, err := a.service.IsAccessGrantedWithBasic(ctx, username, password, model.ManagerAccountNamespace, []model.RoleName{model.ManagerRole}); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

so we have no option to create manager accounts with token? how can we deprecate the basic auth approach then?

Copy link
Contributor

Choose a reason for hiding this comment

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

what about client account for deployer v3?

Copy link
Author

Choose a reason for hiding this comment

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

it is not decided yet. it will be decided when maas becomes an operator. see stage 3 here. https://bass.netcracker.com/display/CPSEC/M2M+MaaS+Design

return utils.LogError(log, ctx, "password verification does not passed: %w", err)
}
if managerAccount, err := a.service.CreateNewManager(ctx, &accountRequest); err != nil {
Expand Down
8 changes: 4 additions & 4 deletions maas/maas-service/controller/account_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func TestAccountController_UpdatePassword(t *testing.T) {
ctx, cancelContext := context.WithCancel(context.Background())
defer cancelContext()

authService := auth.NewAuthService(auth.NewAuthDao(baseDao), nil, nil)
authService := auth.NewAuthService(auth.NewAuthDao(baseDao), nil, nil, nil)
accountController := NewAccountController(authService)

_, err := authService.CreateNewManager(ctx, &model.ManagerAccountDto{
Expand All @@ -72,7 +72,7 @@ func TestAccountController_UpdatePassword(t *testing.T) {
})
assert.NoError(t, err)

app.Put("/auth/account/manager/:name/password", SecurityMiddleware([]model.RoleName{model.ManagerRole}, authService.IsAccessGranted), accountController.UpdatePassword)
app.Put("/auth/account/manager/:name/password", SecurityMiddleware([]model.RoleName{model.ManagerRole}, authService.IsAccessGrantedWithBasic, nil), accountController.UpdatePassword)
testUpdatePasswordPath := "/auth/account/manager/" + managerName + "/password"

req := httptest.NewRequest("PUT", testUpdatePasswordPath, nil)
Expand Down Expand Up @@ -125,7 +125,7 @@ func TestAccountController_DeleteClientAccount(t *testing.T) {
ctx, cancelContext := context.WithCancel(context.Background())
defer cancelContext()

authService := auth.NewAuthService(auth.NewAuthDao(baseDao), nil, nil)
authService := auth.NewAuthService(auth.NewAuthDao(baseDao), nil, nil, nil)
accountController := NewAccountController(authService)

_, err := authService.CreateNewManager(ctx, &model.ManagerAccountDto{
Expand All @@ -149,7 +149,7 @@ func TestAccountController_DeleteClientAccount(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, testClienetUsername, account.Username)

app.Delete("/test", SecurityMiddleware([]model.RoleName{model.ManagerRole}, authService.IsAccessGranted), accountController.DeleteClientAccount)
app.Delete("/test", SecurityMiddleware([]model.RoleName{model.ManagerRole}, authService.IsAccessGrantedWithBasic, nil), accountController.DeleteClientAccount)

userAccountJson, err := json.Marshal(clientAccount)
assert.NoError(t, err)
Expand Down
60 changes: 45 additions & 15 deletions maas/maas-service/controller/controller_common_funcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ import (
"encoding/json"
"errors"
"fmt"
"net/http"
"regexp"
"strconv"
"strings"

"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
Expand All @@ -17,10 +22,6 @@ import (
v "github.com/netcracker/qubership-maas/validator"
"golang.org/x/exp/slices"
"gopkg.in/yaml.v3"
"net/http"
"regexp"
"strconv"
"strings"
)

const (
Expand Down Expand Up @@ -59,28 +60,57 @@ func init() {

type RequestBodyHandler func(ctx context.Context) (interface{}, error)

func SecurityMiddleware(roles []model.RoleName, authorize func(context.Context, string, utils.SecretString, string, []model.RoleName) (*model.Account, error)) fiber.Handler {
type authorizeWithBasicFunc func(context.Context, string, utils.SecretString, string, []model.RoleName) (*model.Account, error)
type authorizeWithTokenFunc func(context.Context, string, string, []model.RoleName) (*model.Account, error)

func SecurityMiddleware(roles []model.RoleName, authorizeWithBasic authorizeWithBasicFunc, authorizeWithToken authorizeWithTokenFunc) fiber.Handler {
return func(ctx *fiber.Ctx) error {
userCtx := ctx.UserContext()
username, password, err := utils.GetBasicAuth(ctx)
if err != nil {
namespace := string(ctx.Request().Header.Peek(HeaderXNamespace))
authHeader := string(ctx.Request().Header.Peek(fiber.HeaderAuthorization))

var (
account *model.Account
// in kubernetes m2m auth composite isolation is always enabled
compositeIsolationDisabled = false
)

authScheme, creds, ok := utils.ParseAuthHeader(authHeader)
if !ok {
if slices.Contains(roles, model.AnonymousRole) {
log.WarnC(userCtx, "Anonymous access will be dropped in future releases for: %s", ctx.OriginalURL())
return ctx.Next()
}
return utils.LogError(log, userCtx, "security middleware error: %w", err)
return utils.LogError(log, userCtx, "request authorization failure: invalid auth header: %w", msg.AuthError)
}

namespace := string(ctx.Request().Header.Peek(HeaderXNamespace))
switch strings.ToLower(authScheme) {
case "basic":
username, password, err := utils.GetBasicAuth(ctx)
if err != nil {
return utils.LogError(log, userCtx, "security middleware error: %w", err)
}

acc, err := authorize(ctx.UserContext(), username, password, namespace, roles)
if err != nil {
return utils.LogError(log, userCtx, "request authorization failure: %w", err)
account, err = authorizeWithBasic(userCtx, username, password, namespace, roles)
if err != nil {
return utils.LogError(log, userCtx, "request authorization failure: %w", err)
}
compositeIsolationDisabled = strings.ToLower(string(ctx.Request().Header.Peek(HeaderXCompositeIsolationDisabled))) == "disabled"
case "bearer":
serviceAccount, err := authorizeWithToken(userCtx, creds, namespace, roles)
if err != nil {
return utils.LogError(log, userCtx, "request authorization failure: %w", err)
}
account = serviceAccount
default:
if slices.Contains(roles, model.AnonymousRole) {
log.WarnC(userCtx, "Anonymous access will be dropped in future releases for: %s", ctx.OriginalURL())
Copy link
Contributor

Choose a reason for hiding this comment

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

code duplication. just check if the role == AnonymousRole in the beginning and skip other steps

return ctx.Next()
}
return utils.LogError(log, userCtx, "security middleware error: %w", msg.AuthError)
}

compositeIsolationDisabled := strings.ToLower(string(ctx.Request().Header.Peek(HeaderXCompositeIsolationDisabled))) == "disabled"

secCtx := model.NewSecurityContext(acc, compositeIsolationDisabled)
secCtx := model.NewSecurityContext(account, compositeIsolationDisabled)
ctx.SetUserContext(model.WithSecurityContext(userCtx, secCtx))
return ctx.Next()
}
Expand Down
Loading
Loading