-
Notifications
You must be signed in to change notification settings - Fork 34
feat: Add FoD Session MFA Code Request Command #1080
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev/v3.x
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| /* | ||
| * 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 com.fasterxml.jackson.databind.JsonNode; | ||
| import com.fasterxml.jackson.databind.node.ObjectNode; | ||
| import com.fortify.cli.common.log.LogSensitivityLevel; | ||
| import com.fortify.cli.common.log.MaskValue; | ||
| import com.fortify.cli.common.output.cli.cmd.AbstractOutputCommand; | ||
| import com.fortify.cli.common.output.cli.cmd.IJsonNodeSupplier; | ||
| import com.fortify.cli.common.output.transform.IActionCommandResultSupplier; | ||
| import com.fortify.cli.common.session.cli.mixin.UserCredentialOptions; | ||
| import com.fortify.cli.fod._common.output.cli.mixin.FoDOutputHelperMixins; | ||
| import com.fortify.cli.fod._common.rest.helper.FoDProductHelper; | ||
| import com.fortify.cli.fod._common.session.cli.mixin.FoDSessionLoginOptions; | ||
| import com.fortify.cli.fod._common.session.helper.FoDMfaDeliveryType; | ||
| import com.fortify.cli.fod._common.session.helper.FoDMfaHelper; | ||
|
|
||
| import lombok.Getter; | ||
| import picocli.CommandLine.Command; | ||
| import picocli.CommandLine.Mixin; | ||
| import picocli.CommandLine.Option; | ||
|
|
||
| /** | ||
| * Command for requesting a Multi-Factor Authentication (MFA) code via Email or SMS. | ||
| * @author Sangamesh Vijaykumar | ||
| */ | ||
| @Command(name = FoDOutputHelperMixins.RequestMfaCode.CMD_NAME, sortOptions = false) | ||
| public class FoDSessionRequestMfaCodeCommand extends AbstractOutputCommand implements IJsonNodeSupplier, IActionCommandResultSupplier { | ||
| @Getter @Mixin private FoDOutputHelperMixins.RequestMfaCode outputHelper; | ||
| @Mixin private FoDSessionLoginOptions.FoDUrlConfigOptions urlConfigOptions; | ||
| @Mixin private UserCredentialOptions userCredentials; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The login command already uses an ArgGroup that declares user, password, and tenant options, right? Why not just re-use that here? |
||
| @Option(names = {"-t", "--tenant"}, required = true) | ||
| @MaskValue(sensitivity = LogSensitivityLevel.low, description = "FOD TENANT") | ||
| private String tenant; | ||
| @Option(names = {"--delivery-mode", "-m"}, required = true) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's keep this option, but make it optional. If not specified, we try both. |
||
| private FoDMfaDeliveryType deliveryMode; | ||
|
|
||
| @Override | ||
| public JsonNode getJsonNode() { | ||
| FoDMfaHelper.requestMfaCode( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please use a similar approach as |
||
| urlConfigOptions, | ||
| tenant, | ||
| userCredentials.getUser(), | ||
| userCredentials.getPassword(), | ||
| deliveryMode | ||
| ); | ||
|
|
||
| String fodUrl = FoDProductHelper.INSTANCE.getBrowserUrl(urlConfigOptions.getUrl()); | ||
|
|
||
| ObjectNode result = com.fortify.cli.common.json.JsonHelper.getObjectMapper().createObjectNode(); | ||
| result.put("fodUrl", fodUrl); | ||
| result.put("deliveryMode", deliveryMode.name()); | ||
| return result; | ||
| } | ||
|
|
||
| @Override | ||
| public boolean isSingular() { | ||
| return true; | ||
| } | ||
|
|
||
| @Override | ||
| public String getActionCommandResult() { | ||
| return "REQUESTED"; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| /* | ||
| * 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.helper; | ||
|
|
||
| import com.formkiq.graalvm.annotations.Reflectable; | ||
|
|
||
| /** | ||
| * Enum representing the delivery types for Multi-Factor Authentication (MFA) codes in Fortify on Demand (FoD). | ||
| * @author Sangamesh Vijaykumar | ||
| */ | ||
| @Reflectable | ||
| public enum FoDMfaDeliveryType { | ||
| Email("EmailDelivery"), | ||
| SMS("SMSDelivery"); | ||
|
|
||
| private final String apiValue; | ||
|
|
||
| FoDMfaDeliveryType(String apiValue) { | ||
| this.apiValue = apiValue; | ||
| } | ||
|
|
||
| public String getApiValue() { | ||
| return apiValue; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| /* | ||
| * 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.helper; | ||
|
|
||
| import com.fasterxml.jackson.databind.node.ObjectNode; | ||
| import com.fortify.cli.common.exception.FcliSimpleException; | ||
| import com.fortify.cli.common.http.proxy.helper.ProxyHelper; | ||
| import com.fortify.cli.common.json.JsonHelper; | ||
| import com.fortify.cli.common.rest.unirest.HttpHeader; | ||
| import com.fortify.cli.common.rest.unirest.UnexpectedHttpResponseException; | ||
| import com.fortify.cli.common.rest.unirest.UnirestHelper; | ||
| import com.fortify.cli.common.rest.unirest.config.IUrlConfig; | ||
| import com.fortify.cli.common.rest.unirest.config.UnirestJsonHeaderConfigurer; | ||
| import com.fortify.cli.common.rest.unirest.config.UnirestUnexpectedHttpResponseConfigurer; | ||
| import com.fortify.cli.common.rest.unirest.config.UnirestUrlConfigConfigurer; | ||
|
|
||
| import kong.unirest.UnirestInstance; | ||
|
|
||
| /** | ||
| * Helper class for requesting Multi-Factor Authentication (MFA) codes in Fortify on Demand (FoD). | ||
| * @author Sangamesh Vijaykumar | ||
| */ | ||
| public class FoDMfaHelper { | ||
|
|
||
| public static final void requestMfaCode(IUrlConfig urlConfig, String tenant, String user, char[] password, FoDMfaDeliveryType deliveryType) { | ||
| try ( var unirest = UnirestHelper.createUnirestInstance() ) { | ||
| configureUnirest(unirest, urlConfig); | ||
|
|
||
| ObjectNode requestBody = JsonHelper.getObjectMapper().createObjectNode(); | ||
| requestBody.put("multiFactorAuthorizationType", deliveryType.getApiValue()); | ||
| requestBody.put("username", String.format("%s\\%s", tenant, user)); | ||
| requestBody.put("password", String.valueOf(password)); | ||
|
|
||
| unirest.post("/api/v3/multi-factor-authorization-code") | ||
| .headerReplace(HttpHeader.ACCEPT, "application/json") | ||
| .headerReplace(HttpHeader.CONTENT_TYPE, "application/json") | ||
| .body(requestBody) | ||
| .asEmpty(); | ||
|
|
||
| //security hardening | ||
| java.util.Arrays.fill(password, ' '); // Clear original char array | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| } catch ( UnexpectedHttpResponseException e ) { | ||
| if ( e.getStatus() == 400 ) { | ||
| throw new FcliSimpleException( | ||
| "MFA is not enabled for this tenant, or the provided credentials are invalid." | ||
| + " Contact your FoD administrator, then try again." | ||
| ); | ||
| } else if ( e.getStatus() == 401 || e.getStatus() == 403 ) { | ||
| throw new FcliSimpleException( | ||
| "Authentication failed: invalid username, tenant, or password." | ||
| ); | ||
| } | ||
| throw e; | ||
| } | ||
| } | ||
|
|
||
| private static void configureUnirest(UnirestInstance unirest, IUrlConfig urlConfig) { | ||
| UnirestUnexpectedHttpResponseConfigurer.configure(unirest); | ||
| UnirestUrlConfigConfigurer.configure(unirest, urlConfig); | ||
| ProxyHelper.configureProxy(unirest, "fod", urlConfig.getUrl()); | ||
| UnirestJsonHeaderConfigurer.configure(unirest); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Although it's nice to have a short alias, I'm not 100% sure whether I like this alias ('mfa' by itself has a meaning, but doesn't describe what this command does, and also maybe in the future we need to add other MFA-related commands).
Related, can we think of a shorter full command name? I think the option on the login command is now
--code, so maybe we can name the commandrequest-code? Any other word for 'request'? Maybe ask Shajaan for suggestions?Maybe we should reconsider this altogether, thinking first about whether we should improve the MFA handling on the
logincommand. Some potential approaches:logincommand:--mfa=<code>and--totp=<code>, then have separatemfacommand to request MFA code (thus command name matching thelogin--mfaoption name)logincommand, i.e.,--mfa[=<code>], if no code given, we check whether there's a cached code; if not (or cached code results in denied exception due to being expired), we send the 'request MFA' request and then prompt the user to enter the MFA codeIn other words, maybe we should take a step back to decide on the most user-friendly approach (while maintaining backward compatibility, possibly marking existing functionality as deprecated).