-
Notifications
You must be signed in to change notification settings - Fork 34
feat: fcli fod session login: Add support for TOTP and multi-factor authentication (MFA) codes in the FoD session login command
#1073
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
Changes from all commits
2fc2fb3
960a32f
c231500
fafddda
334b3f0
1621490
8a14e1c
7aa3cdb
6b6648e
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 |
|---|---|---|
|
|
@@ -22,7 +22,10 @@ | |
| import com.fortify.cli.common.session.cli.mixin.UserCredentialOptions; | ||
| import com.fortify.cli.fod._common.rest.helper.FoDProductHelper; | ||
| import com.fortify.cli.fod._common.session.helper.oauth.IFoDClientCredentials; | ||
| import com.fortify.cli.fod._common.session.helper.oauth.IFoDUserAuthCode; | ||
| import com.fortify.cli.fod._common.session.helper.oauth.IFoDUserCredentials; | ||
| import com.fortify.cli.fod._common.session.helper.oauth.impl.BasicFoDUserAuthCode; | ||
| import com.fortify.cli.fod._common.session.helper.oauth.impl.BasicFoDUserCredentials; | ||
|
|
||
| import lombok.Getter; | ||
| import lombok.SneakyThrows; | ||
|
|
@@ -33,28 +36,33 @@ | |
|
|
||
| public class FoDSessionLoginOptions { | ||
| @Mixin @Getter private FoDUrlConfigOptions urlConfigOptions = new FoDUrlConfigOptions(); | ||
|
|
||
| @ArgGroup(exclusive = false, multiplicity = "1", order = 2) | ||
| @Getter private FoDAuthOptions authOptions = new FoDAuthOptions(); | ||
|
|
||
| public static class FoDAuthOptions { | ||
| @ArgGroup(exclusive = true, multiplicity = "1", order = 3) | ||
| @Getter private FoDCredentialOptions credentialOptions = new FoDCredentialOptions(); | ||
| @Option(names="--scopes", defaultValue="api-tenant", split=",") | ||
| @Getter private String[] scopes; | ||
| } | ||
|
|
||
| public static class FoDCredentialOptions { | ||
| @ArgGroup(exclusive = false, multiplicity = "1", order = 1) | ||
| @ArgGroup(exclusive = false, multiplicity = "1", order = 1) | ||
| @Getter private FoDUserCredentialOptions userCredentialOptions = new FoDUserCredentialOptions(); | ||
| @ArgGroup(exclusive = false, multiplicity = "1", order = 2) | ||
| @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; | ||
| @Option(names = {"--code", "-c" }, paramLabel = "<code>", arity = "0..1", interactive = true, echo = false) | ||
| @MaskValue(sensitivity = LogSensitivityLevel.low, description = "FOD TOTP/MFA CODE") | ||
| @Getter private String securityCode; | ||
| @Option(names = {"--totp" }) | ||
| @Getter private boolean isTotp; | ||
| } | ||
|
|
||
| public static class FoDClientCredentialOptions implements IFoDClientCredentials { | ||
|
|
@@ -72,7 +80,7 @@ public FoDUserCredentialOptions getUserCredentialOptions() { | |
| .map(FoDCredentialOptions::getUserCredentialOptions) | ||
| .orElse(null); | ||
| } | ||
|
|
||
| public FoDClientCredentialOptions getClientCredentialOptions() { | ||
| return Optional.ofNullable(authOptions) | ||
| .map(FoDAuthOptions::getCredentialOptions) | ||
|
|
@@ -89,55 +97,56 @@ public final boolean hasUserCredentials() { | |
| && userCredentialOptions.getPassword().length > 0; | ||
| } | ||
|
|
||
| public final BasicFoDUserCredentials getUserCredentials() { | ||
| public final IFoDUserCredentials getUserCredentials() { | ||
| var u = getUserCredentialOptions(); | ||
| return BasicFoDUserCredentials.builder().tenant(u.getTenant()).user(u.getUser()).password(u.getPassword()).build(); | ||
| return BasicFoDUserCredentials.builder() | ||
| .tenant(u.getTenant()) | ||
| .user(u.getUser()) | ||
| .password(u.getPassword()) | ||
| .build(); | ||
| } | ||
|
|
||
| public final boolean hasClientCredentials() { | ||
| FoDClientCredentialOptions clientCredentialOptions = getClientCredentialOptions(); | ||
| return clientCredentialOptions!=null | ||
| && StringUtils.isNotBlank(clientCredentialOptions.getClientId()) | ||
| && StringUtils.isNotBlank(clientCredentialOptions.getClientSecret()); | ||
| } | ||
|
|
||
|
|
||
| public boolean hasSecurityCode() { | ||
| var userCred = getUserCredentialOptions(); | ||
| return userCred != null && StringUtils.isNotBlank(userCred.getSecurityCode()); | ||
| } | ||
|
|
||
| public String getSecurityCode() { | ||
| var userCred = getUserCredentialOptions(); | ||
| return userCred != null ? userCred.getSecurityCode() : null; | ||
| } | ||
|
|
||
| public boolean isTotp() { | ||
| var userCred = getUserCredentialOptions(); | ||
| return userCred != null && userCred.isTotp(); | ||
| } | ||
|
|
||
| public IFoDUserAuthCode getAuthCode() { | ||
| var u = getUserCredentialOptions(); | ||
| if (u == null || StringUtils.isBlank(u.getSecurityCode())) { return null; } | ||
| return BasicFoDUserAuthCode.builder() | ||
| .securityCode(u.getSecurityCode()) | ||
| .isTotp(u.isTotp()) | ||
| .build(); | ||
| } | ||
|
|
||
| @Command | ||
| public static final class FoDUrlConfigOptions extends UrlConfigOptions { | ||
| @Override @SneakyThrows | ||
| public String getUrl() { | ||
| return FoDProductHelper.INSTANCE.getApiUrl(super.getUrl()); | ||
| } | ||
|
|
||
| @Override | ||
| protected int getDefaultSocketTimeoutInMillis() { | ||
| return 600000; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Basic immutable FoD user credentials with builder pattern. | ||
| */ | ||
| public static final class BasicFoDUserCredentials implements IFoDUserCredentials { | ||
|
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. I think (but please double-check) that Can you please check:
To summarize:
Contributor
Author
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 Basic* classes are kept as top-level classes. please review the code now. |
||
| private final String tenant; | ||
| private final String user; | ||
| private final char[] password; | ||
| private BasicFoDUserCredentials(Builder b) { | ||
| this.tenant = b.tenant; | ||
| this.user = b.user; | ||
| this.password = b.password; | ||
| } | ||
| public static Builder builder() { return new Builder(); } | ||
| @Override public String getTenant() { return tenant; } | ||
| @Override public String getUser() { return user; } | ||
| @Override public char[] getPassword() { return password; } | ||
| public static final class Builder { | ||
| private String tenant; private String user; private char[] password; | ||
| public Builder tenant(String tenant){ this.tenant=tenant; return this; } | ||
| public Builder user(String user){ this.user=user; return this; } | ||
| public Builder password(char[] password){ this.password=password; return this; } | ||
| public BasicFoDUserCredentials build(){ | ||
| return new BasicFoDUserCredentials(this); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| /* | ||
| * 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.oauth; | ||
|
|
||
| public interface IFoDUserAuthCode { | ||
| default String getSecurityCode() { return null; } | ||
| default boolean isTotp() { return false; } | ||
| } |
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.
FcliSimpleExceptionwith appropriate guidance (like client credentials incorrect or expired)?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.
I have had class-level constants created for the messages and have used them.
Note: Across the entire codebase, every other command throws
FcliSimpleExceptionwith the message string inline at the throw site, either as a string literal directly or viaString.format(...). No other command pre-defines multi-line message constants likeMFA_GUIDANCE,ERROR_WITH_CODE, andERROR_WITHOUT_CODE.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.
Added dedicated exception handling for client credentials as well.