Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
import picocli.CommandLine.Command;
import picocli.CommandLine.Mixin;

@Command(name = OutputHelperMixins.Login.CMD_NAME, sortOptions = false)
@Command(name = OutputHelperMixins.Login.CMD_NAME, sortOptions = false, preprocessor = FoDSessionTenantIgnoringPreprocessor.class)
public class FoDSessionLoginCommand extends AbstractSessionLoginCommand<FoDSessionDescriptor> {
@Getter @Mixin private OutputHelperMixins.Login outputHelper;
@Getter private FoDSessionHelper sessionHelper = FoDSessionHelper.instance();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Copyright 2021-2026 Open Text.
*
* The only warranties for products and services of Open Text
* and its affiliates and licensors ("Open Text") are as may
* be set forth in the express warranty statements accompanying
* such products and services. Nothing herein should be construed
* as constituting an additional warranty. Open Text shall not be
* liable for technical or editorial errors or omissions contained
* herein. The information contained herein is subject to change
* without notice.
*/
package com.fortify.cli.fod._common.session.cli.cmd;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Stack;

import com.formkiq.graalvm.annotations.Reflectable;

import picocli.CommandLine.IParameterPreprocessor;
import picocli.CommandLine.Model.ArgSpec;
import picocli.CommandLine.Model.CommandSpec;

/**
* Removes --tenant/-t from command line arguments when client credentials are used.
* This allows tenant to stay mandatory in the user credential argument group while
* accepting tenant as a no-op for client credential authentication.
*
* @author Sangamesh Vijayakumar
*/
@Reflectable
public final class FoDSessionTenantIgnoringPreprocessor implements IParameterPreprocessor {
@Override
public boolean preprocess(Stack<String> args, CommandSpec commandSpec, ArgSpec argSpec, Map<String, Object> info) {
if ( argSpec!=null || args==null || args.isEmpty() ) {
return false;
}

var cliArgs = new ArrayList<>(args);
if ( !hasClientCredentials(cliArgs) ) {
return false;
}

var filtered = filterOutTenantOptions(cliArgs);
if ( filtered.size()!=cliArgs.size() ) {
args.clear();
args.addAll(filtered);
}
return false;
}

private static boolean hasClientCredentials(List<String> cliArgs) {
return cliArgs.stream().anyMatch(a -> "--client-id".equals(a)
|| a.startsWith("--client-id=")
|| "--client-secret".equals(a)
|| a.startsWith("--client-secret="));
}

private static boolean isCompactTenantOption(String arg) {
return arg.startsWith("-t") && arg.length() > 2 && !arg.startsWith("--");
}

private static List<String> filterOutTenantOptions(List<String> cliArgs) {
var result = new ArrayList<String>();
for ( int i = 0; i < cliArgs.size(); i++ ) {
var arg = cliArgs.get(i);
if ( "--tenant".equals(arg) || "-t".equals(arg) ) {
if ( i+1 < cliArgs.size() && !cliArgs.get(i+1).startsWith("-") ) {
i++; // Skip explicit tenant value.
}
continue;
}
if ( arg.startsWith("--tenant=") || arg.startsWith("-t=") || isCompactTenantOption(arg) ) {
continue;
}
result.add(arg);
}
return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

import org.apache.commons.lang3.StringUtils;

import com.fortify.cli.common.exception.FcliSimpleException;
import com.fortify.cli.common.log.LogSensitivityLevel;
import com.fortify.cli.common.log.MaskValue;
import com.fortify.cli.common.rest.cli.mixin.UrlConfigOptions;
Expand All @@ -43,18 +42,21 @@ public static class FoDAuthOptions {
@Getter private FoDCredentialOptions credentialOptions = new FoDCredentialOptions();
@Option(names="--scopes", defaultValue="api-tenant", split=",")
@Getter private String[] scopes;
@Option(names = {"-t", "--tenant"}, required = false)
@MaskValue(sensitivity = LogSensitivityLevel.low, description = "FOD TENANT")
@Getter private String tenant; // Optional: required only for user credentials
}

public static class FoDCredentialOptions {
@ArgGroup(exclusive = false, multiplicity = "1", order = 1)
@Getter private UserCredentialOptions userCredentialOptions = new UserCredentialOptions();
@Getter private FoDUserCredentialOptions userCredentialOptions = new FoDUserCredentialOptions();
@ArgGroup(exclusive = false, multiplicity = "1", order = 2)
@Getter private FoDClientCredentialOptions clientCredentialOptions = new FoDClientCredentialOptions();
}

public static class FoDUserCredentialOptions extends UserCredentialOptions {
@Option(names = {"-t", "--tenant"}, required = true)
@MaskValue(sensitivity = LogSensitivityLevel.low, description = "FOD TENANT")
@Getter private String tenant;
}

public static class FoDClientCredentialOptions implements IFoDClientCredentials {
@Option(names = {"--client-id"}, required = true)
@MaskValue(sensitivity = LogSensitivityLevel.medium, description = "FOD CLIENT ID")
Expand All @@ -64,7 +66,7 @@ public static class FoDClientCredentialOptions implements IFoDClientCredentials
@Getter private String clientSecret;
}

public UserCredentialOptions getUserCredentialOptions() {
public FoDUserCredentialOptions getUserCredentialOptions() {
return Optional.ofNullable(authOptions)
.map(FoDAuthOptions::getCredentialOptions)
.map(FoDCredentialOptions::getUserCredentialOptions)
Expand All @@ -79,16 +81,17 @@ public FoDClientCredentialOptions getClientCredentialOptions() {
}

public final boolean hasUserCredentials() {
return getUserCredentialOptions()!=null;
var userCredentialOptions = getUserCredentialOptions();
return userCredentialOptions!=null
&& StringUtils.isNotBlank(userCredentialOptions.getTenant())
&& StringUtils.isNotBlank(userCredentialOptions.getUser())
&& userCredentialOptions.getPassword()!=null
&& userCredentialOptions.getPassword().length > 0;
}

public final BasicFoDUserCredentials getUserCredentials() {
var u = getUserCredentialOptions();
var t = Optional.ofNullable(authOptions).map(FoDAuthOptions::getTenant).orElse(null);
if ( u==null || StringUtils.isBlank(t) || StringUtils.isBlank(u.getUser()) || u.getPassword()==null ) {
throw new FcliSimpleException("--tenant, --user and --password must all be specified for user credential authentication");
}
return BasicFoDUserCredentials.builder().tenant(t).user(u.getUser()).password(u.getPassword()).build();
return BasicFoDUserCredentials.builder().tenant(u.getTenant()).user(u.getUser()).password(u.getPassword()).build();
}

public final boolean hasClientCredentials() {
Expand Down Expand Up @@ -133,9 +136,6 @@ public static final class Builder {
public Builder user(String user){ this.user=user; return this; }
public Builder password(char[] password){ this.password=password; return this; }
public BasicFoDUserCredentials build(){
if ( StringUtils.isBlank(tenant) || StringUtils.isBlank(user) || password==null || password.length==0 ) {
throw new FcliSimpleException("--tenant, --user and --password must all be specified for user credential authentication");
}
return new BasicFoDUserCredentials(this);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ fcli.fod.session.login.usage.description.2 = %nTo avoid having to remember the v
DEV_FOD_TENANT environment variables, and then use the --env-prefix=PROD or --env-prefix=DEV option to \
select from which environment variables the default values should be retrieved.
fcli.fod.session.login.url = FoD URL, for example https://emea.fortify.com/.
fcli.fod.session.login.tenant = FoD tenant; required when authenticating with user credentials, ignored for client credentials.
fcli.fod.session.login.tenant = FoD tenant; required, but ignored for client credentials.
fcli.fod.session.login.user = FoD user.
fcli.fod.session.login.password = FoD password.
fcli.fod.session.login.client-id = FoD client id.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
* Copyright 2021-2026 Open Text.
*
* The only warranties for products and services of Open Text
* and its affiliates and licensors ("Open Text") are as may
* be set forth in the express warranty statements accompanying
* such products and services. Nothing herein should be construed
* as constituting an additional warranty. Open Text shall not be
* liable for technical or editorial errors or omissions contained
* herein. The information contained herein is subject to change
* without notice.
*/
package com.fortify.cli.fod._common.session.cli.cmd;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.ArrayList;
import java.util.Collections;

import org.junit.jupiter.api.Test;

import com.fortify.cli.fod._common.session.cli.mixin.FoDSessionLoginOptions;

import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.MissingParameterException;
import picocli.CommandLine.Mixin;
import picocli.CommandLine.UnmatchedArgumentException;

class FoDSessionTenantIgnoringPreprocessorTest {
@Test
void shouldAllowClientCredentialsWithoutTenant() {
var cmd = parse("--client-id", "id", "--client-secret", "secret");

assertTrue(cmd.loginOptions.hasClientCredentials());
assertFalse(cmd.loginOptions.hasUserCredentials());
}

@Test
void shouldIgnoreTenantWhenClientCredentialsProvided() {
var cmd = parse("--tenant", "acme", "--client-id", "id", "--client-secret", "secret");

assertTrue(cmd.loginOptions.hasClientCredentials());
assertFalse(cmd.loginOptions.hasUserCredentials());
}

@Test
void shouldIgnoreCompactTenantWhenClientCredentialsProvided() {
var cmd = parse("-tacme", "--client-id", "id", "--client-secret", "secret");

assertTrue(cmd.loginOptions.hasClientCredentials());
assertFalse(cmd.loginOptions.hasUserCredentials());
}

@Test
void shouldIgnoreAllSupportedTenantSyntaxWhenClientCredentialsProvided() {
assertClientCredentialsOnly("-t", "acme");
assertClientCredentialsOnly("-t=acme");
assertClientCredentialsOnly("-tacme");
assertClientCredentialsOnly("--tenant", "acme");
assertClientCredentialsOnly("--tenant=acme");
}

@Test
void shouldRejectInvalidLongTenantSyntax() {
assertThrows(UnmatchedArgumentException.class,
() -> parse("--tenanttenant-value", "--client-id", "id", "--client-secret", "secret"));
}

@Test
void shouldFailUserCredentialsWithoutTenant() {
var ex = assertThrows(MissingParameterException.class,
() -> parse("--user", "bob", "--password", "pw"));

assertTrue(ex.getMessage().toLowerCase().contains("tenant"));
}

@Test
void shouldAllowUserCredentialsWithTenant() {
var cmd = parse("--tenant", "acme", "--user", "bob", "--password", "pw");

assertTrue(cmd.loginOptions.hasUserCredentials());
assertDoesNotThrow(() -> cmd.loginOptions.getUserCredentials());
}

private static void assertClientCredentialsOnly(String... tenantArgs) {
var args = new ArrayList<String>();
Collections.addAll(args, tenantArgs);
args.add("--client-id");
args.add("id");
args.add("--client-secret");
args.add("secret");

var cmd = parse(args.toArray(String[]::new));
assertTrue(cmd.loginOptions.hasClientCredentials());
assertFalse(cmd.loginOptions.hasUserCredentials());
}

private static TestFoDLoginCommand parse(String... args) {
var cmd = new TestFoDLoginCommand();
var fullArgs = new ArrayList<String>();
fullArgs.add("--url");
fullArgs.add("https://example.org");
Collections.addAll(fullArgs, args);
new CommandLine(cmd).parseArgs(fullArgs.toArray(String[]::new));
return cmd;
}

@Command(name = "test-fod-login", preprocessor = FoDSessionTenantIgnoringPreprocessor.class)
static final class TestFoDLoginCommand {
@Mixin private FoDSessionLoginOptions loginOptions;
}
}
Loading