diff --git a/cli/pom.xml b/cli/pom.xml index 54c7dab2..6cbc18d4 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -71,16 +71,8 @@ org.slf4j - slf4j-simple + slf4j-jdk14 ${slf4j.version} - runtime - - - - org.slf4j - jcl-over-slf4j - ${slf4j.version} - runtime diff --git a/cli/src/main/graalvm/reflect-config.json b/cli/src/main/graalvm/reflect-config.json index 2985268c..ee0a0cf5 100644 --- a/cli/src/main/graalvm/reflect-config.json +++ b/cli/src/main/graalvm/reflect-config.json @@ -29,6 +29,13 @@ "allPublicMethods": true, "allPublicFields": true }, + { + "name": "com.okta.cli.common.model.UserProfileRequestWrapper", + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allPublicMethods": true, + "allPublicFields": true + }, { "name": "com.okta.cli.common.model.OrganizationResponse", "allDeclaredFields": true, diff --git a/cli/src/main/java/com/okta/cli/OktaCli.java b/cli/src/main/java/com/okta/cli/OktaCli.java index 0e9a8190..26791466 100644 --- a/cli/src/main/java/com/okta/cli/OktaCli.java +++ b/cli/src/main/java/com/okta/cli/OktaCli.java @@ -30,6 +30,10 @@ import picocli.CommandLine.Spec; import java.util.List; +import java.util.logging.ConsoleHandler; +import java.util.logging.Level; +import java.util.logging.LogManager; +import java.util.logging.Logger; @Command(name = "okta", description = "The Okta CLI helps you configure your applications to use Okta.", @@ -110,9 +114,18 @@ public void setBatch(boolean batch) { @Option(names = "--verbose", description = "Verbose logging.") public void setVerbose(boolean verbose) { - this.environment.setVerbose(verbose); + environment.setVerbose(verbose); if (verbose) { - System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "debug"); + // + System.setProperty("java.util.logging.SimpleFormatter.format", "%1$tFT%1$tT.%1$tL%1$tz %4$s %2$s - %5$s\u001F%6$s%n"); + + final LogManager logManager = LogManager.getLogManager(); + Logger rootLogger = logManager.getLogger(""); + rootLogger.setLevel(Level.FINER); + + ConsoleHandler consoleHandler = new ConsoleHandler(); + consoleHandler.setLevel(Level.FINER); + rootLogger.addHandler(consoleHandler); } } diff --git a/cli/src/main/java/com/okta/cli/commands/Register.java b/cli/src/main/java/com/okta/cli/commands/Register.java index d1178e44..27be6787 100644 --- a/cli/src/main/java/com/okta/cli/commands/Register.java +++ b/cli/src/main/java/com/okta/cli/commands/Register.java @@ -23,8 +23,11 @@ import com.okta.cli.common.service.DefaultSdkConfigurationService; import com.okta.cli.common.service.DefaultSetupService; import com.okta.cli.common.service.SetupService; +import com.okta.cli.common.service.UserCanceledException; import com.okta.cli.console.ConsoleOutput; import com.okta.cli.console.Prompter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import picocli.CommandLine; import picocli.CommandLine.Command; @@ -32,6 +35,8 @@ description = "Sign up for a new Okta account") public class Register extends BaseCommand { + private final Logger logger = LoggerFactory.getLogger(Register.class); + @CommandLine.Option(names = "--email", description = "Email used when registering a new Okta account.") protected String email; @@ -44,6 +49,12 @@ public class Register extends BaseCommand { @CommandLine.Option(names = "--company", description = "Company/organization used when registering a new Okta account.") protected String company; + @CommandLine.Option(names = "--country", description = "Country of residence") + protected String country; + + @CommandLine.Option(names = "--oie", description = "Create Okta account with OIE enabled.", defaultValue = "true", hidden = true, negatable = true) + protected Boolean oie = Boolean.TRUE; + public Register() {} private Register(OktaCli.StandardOptions standardOptions) { @@ -70,17 +81,21 @@ public int runCommand() throws Exception { CliRegistrationQuestions registrationQuestions = registrationQuestions(); - SetupService setupService = new DefaultSetupService(OidcProperties.oktaEnv()); - OrganizationResponse orgResponse = setupService.createOktaOrg(registrationQuestions, - getEnvironment().getOktaPropsFile(), - getEnvironment().isDemo(), - getEnvironment().isInteractive()); - - String identifier = orgResponse.getId(); - setupService.verifyOktaOrg(identifier, - registrationQuestions, - getEnvironment().getOktaPropsFile()); - + try { + SetupService setupService = new DefaultSetupService(OidcProperties.oktaEnv()); + OrganizationResponse orgResponse = setupService.createOktaOrg(registrationQuestions, + getEnvironment().getOktaPropsFile(), + getEnvironment().isDemo(), + getEnvironment().isInteractive()); + + String identifier = orgResponse.getDeveloperOrgCliToken(); + setupService.verifyOktaOrg(identifier, + registrationQuestions, + getEnvironment().getOktaPropsFile()); + } catch (UserCanceledException e) { + logger.debug("User canceled registration.", e); + return 2; + } return 0; @@ -104,12 +119,8 @@ public OrganizationRequest getOrganizationRequest() { .setFirstName(prompter.promptUntilValue(firstName, "First name")) .setLastName(prompter.promptUntilValue(lastName, "Last name")) .setEmail(prompter.promptUntilValue(email, "Email address")) - .setOrganization(prompter.promptUntilValue(company, "Company")); - } - - @Override - public String getVerificationCode() { - return prompter.promptUntilValue("Verification code"); + .setCountry(prompter.promptUntilValue(country, "Country")) + .setOie(oie); } } } diff --git a/common/src/main/java/com/okta/cli/common/model/OrganizationRequest.java b/common/src/main/java/com/okta/cli/common/model/OrganizationRequest.java index b6241ff1..bc660cf3 100755 --- a/common/src/main/java/com/okta/cli/common/model/OrganizationRequest.java +++ b/common/src/main/java/com/okta/cli/common/model/OrganizationRequest.java @@ -15,15 +15,21 @@ */ package com.okta.cli.common.model; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; import lombok.experimental.Accessors; @Data @Accessors(chain = true) +@JsonInclude(JsonInclude.Include.NON_NULL) public class OrganizationRequest { private String firstName; private String lastName; private String email; - private String organization; + private String country; + + @JsonProperty("okta_oie") + private Boolean oie; } diff --git a/common/src/main/java/com/okta/cli/common/model/OrganizationResponse.java b/common/src/main/java/com/okta/cli/common/model/OrganizationResponse.java index 3ff06fd1..424c54e3 100755 --- a/common/src/main/java/com/okta/cli/common/model/OrganizationResponse.java +++ b/common/src/main/java/com/okta/cli/common/model/OrganizationResponse.java @@ -22,11 +22,12 @@ @Accessors(chain = true) public class OrganizationResponse { - private String id; private String orgUrl; - private String email; private String apiToken; - private String factorId; - private String updatePasswordUrl; + private String developerOrgCliToken; + private String status; // ACTIVE or PENDING + public boolean isActive() { + return "ACTIVE".equals(status); + } } diff --git a/common/src/main/java/com/okta/cli/common/model/RegistrationQuestions.java b/common/src/main/java/com/okta/cli/common/model/RegistrationQuestions.java index 42be5cff..c9b3a77f 100644 --- a/common/src/main/java/com/okta/cli/common/model/RegistrationQuestions.java +++ b/common/src/main/java/com/okta/cli/common/model/RegistrationQuestions.java @@ -21,21 +21,17 @@ public interface RegistrationQuestions { OrganizationRequest getOrganizationRequest(); - String getVerificationCode(); - - static RegistrationQuestions answers(boolean overwriteConfig, OrganizationRequest request, String code) { - return new SimpleRegistrationQuestions(overwriteConfig, request, code); + static RegistrationQuestions answers(boolean overwriteConfig, OrganizationRequest request) { + return new SimpleRegistrationQuestions(overwriteConfig, request); } class SimpleRegistrationQuestions implements RegistrationQuestions { private final boolean overwriteConfig; private final OrganizationRequest organizationRequest; - private final String verificationCode; - public SimpleRegistrationQuestions(boolean overwriteConfig, OrganizationRequest organizationRequest, String verificationCode) { + public SimpleRegistrationQuestions(boolean overwriteConfig, OrganizationRequest organizationRequest) { this.overwriteConfig = overwriteConfig; this.organizationRequest = organizationRequest; - this.verificationCode = verificationCode; } public boolean isOverwriteExistingConfig(String oktaBaseUrl, String configFile) { @@ -45,9 +41,5 @@ public boolean isOverwriteExistingConfig(String oktaBaseUrl, String configFile) public OrganizationRequest getOrganizationRequest() { return organizationRequest; } - - public String getVerificationCode() { - return verificationCode; - } } } diff --git a/common/src/main/java/com/okta/cli/common/FactorVerificationException.java b/common/src/main/java/com/okta/cli/common/model/UserProfileRequestWrapper.java similarity index 60% rename from common/src/main/java/com/okta/cli/common/FactorVerificationException.java rename to common/src/main/java/com/okta/cli/common/model/UserProfileRequestWrapper.java index 876500d0..8f5eedd9 100644 --- a/common/src/main/java/com/okta/cli/common/FactorVerificationException.java +++ b/common/src/main/java/com/okta/cli/common/model/UserProfileRequestWrapper.java @@ -13,17 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.okta.cli.common; +package com.okta.cli.common.model; -import com.okta.cli.common.model.ErrorResponse; +import lombok.Data; +import lombok.experimental.Accessors; -public class FactorVerificationException extends RestException { +/** + * Okta's Registration API wraps the request in a "userProfile" node. + */ +@Data +@Accessors(chain = true) +public class UserProfileRequestWrapper { - public FactorVerificationException(ErrorResponse errorResponse, Throwable t) { - super(errorResponse, t); - } + private final OrganizationRequest userProfile; - public FactorVerificationException(ErrorResponse errorResponse) { - super(errorResponse); + public UserProfileRequestWrapper(OrganizationRequest userProfile) { + this.userProfile = userProfile; } -} +} \ No newline at end of file diff --git a/common/src/main/java/com/okta/cli/common/service/DefaultOktaOrganizationCreator.java b/common/src/main/java/com/okta/cli/common/service/DefaultOktaOrganizationCreator.java index 59901b12..beae6e66 100644 --- a/common/src/main/java/com/okta/cli/common/service/DefaultOktaOrganizationCreator.java +++ b/common/src/main/java/com/okta/cli/common/service/DefaultOktaOrganizationCreator.java @@ -15,32 +15,27 @@ */ package com.okta.cli.common.service; -import com.okta.cli.common.FactorVerificationException; import com.okta.cli.common.RestException; import com.okta.cli.common.model.OrganizationRequest; import com.okta.cli.common.model.OrganizationResponse; +import com.okta.cli.common.model.UserProfileRequestWrapper; import java.io.IOException; public class DefaultOktaOrganizationCreator implements OktaOrganizationCreator { - private final RestClient restClient = new DefaultStartRestClient(); + private final RestClient restClient = new DefaultStartRestClient(Settings.getRegistrationBaseUrl()); + + private final String registrationId = Settings.getRegistrationId(); @Override public OrganizationResponse createNewOrg(OrganizationRequest orgRequest) throws RestException, IOException { - - return restClient.post("/create", orgRequest, OrganizationResponse.class); + return restClient.post("/api/v1/registration/" + registrationId + "/register", new UserProfileRequestWrapper(orgRequest), OrganizationResponse.class); } @Override - public OrganizationResponse verifyNewOrg(String identifier, String code) throws FactorVerificationException, IOException { - - String postBody = "{\"code\":\"" + code + "\"}"; - - try { - return restClient.post("/verify/" + identifier, postBody, OrganizationResponse.class); - } catch (RestException e) { - throw new FactorVerificationException(e.getErrorResponse(), e); - } + public OrganizationResponse verifyNewOrg(String identifier) throws RestException, IOException { + return restClient.get("/api/internal/v1/developer/redeem/" + identifier, OrganizationResponse.class); } + } \ No newline at end of file diff --git a/common/src/main/java/com/okta/cli/common/service/DefaultSetupService.java b/common/src/main/java/com/okta/cli/common/service/DefaultSetupService.java index e0d93fca..e52d3483 100644 --- a/common/src/main/java/com/okta/cli/common/service/DefaultSetupService.java +++ b/common/src/main/java/com/okta/cli/common/service/DefaultSetupService.java @@ -15,7 +15,6 @@ */ package com.okta.cli.common.service; -import com.okta.cli.common.FactorVerificationException; import com.okta.cli.common.RestException; import com.okta.cli.common.config.MutablePropertySource; import com.okta.cli.common.model.OidcProperties; @@ -30,6 +29,7 @@ import com.okta.sdk.impl.config.ClientConfiguration; import com.okta.sdk.impl.resource.DefaultGroupBuilder; import com.okta.sdk.resource.ExtensibleResource; +import com.okta.sdk.resource.ResourceException; import com.okta.sdk.resource.application.OpenIdConnectApplicationType; import com.okta.sdk.resource.group.Group; import com.okta.sdk.resource.group.GroupList; @@ -43,6 +43,7 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.StandardCopyOption; +import java.time.Duration; import java.time.Instant; import java.time.ZoneId; import java.time.format.DateTimeFormatter; @@ -62,6 +63,7 @@ public class DefaultSetupService implements SetupService { private final OidcProperties oidcProperties; + private final Duration verificationPollingFrequency = Duration.ofSeconds(4); public DefaultSetupService(OidcProperties oidcProperties) { this(new DefaultSdkConfigurationService(), @@ -87,18 +89,17 @@ public DefaultSetupService(SdkConfigurationService sdkConfigurationService, public OrganizationResponse createOktaOrg(RegistrationQuestions registrationQuestions, File oktaPropsFile, boolean demo, - boolean interactive) throws IOException, ClientConfigurationException { + boolean interactive) throws IOException, ClientConfigurationException, UserCanceledException { // check if okta client config exists? ClientConfiguration clientConfiguration = sdkConfigurationService.loadUnvalidatedConfiguration(); - String orgUrl; try (ProgressBar progressBar = ProgressBar.create(interactive)) { if (!Strings.isEmpty(clientConfiguration.getBaseUrl())) { if (!registrationQuestions.isOverwriteExistingConfig(clientConfiguration.getBaseUrl(), oktaPropsFile.getAbsolutePath())) { - throw new ClientConfigurationException("User canceled"); + throw new UserCanceledException(); } Instant instant = Instant.now(); @@ -115,43 +116,42 @@ public OrganizationResponse createOktaOrg(RegistrationQuestions registrationQues try { OrganizationResponse newOrg = organizationCreator.createNewOrg(organizationRequest); - orgUrl = newOrg.getOrgUrl(); - - progressBar.info("OrgUrl: " + orgUrl); - progressBar.info("An email has been sent to you with a verification code."); + progressBar.info("An account activation email has been sent to you."); return newOrg; - } catch (RestException e) { - throw new ClientConfigurationException("Failed to create Okta Organization. You can register " + - "manually by going to https://developer.okta.com/signup"); + } catch (RestException | ResourceException e) { + throw new ClientConfigurationException(e.getMessage() + + // the original REST exception likely contained information about why this failed + "\nFailed to create Okta Organization. You can register manually by going " + + "to https://developer.okta.com/signup", e); } } } - @Override - public void verifyOktaOrg(String identifier, RegistrationQuestions registrationQuestions, File oktaPropsFile) throws IOException, ClientConfigurationException { + public void verifyOktaOrg(String identifier, RegistrationQuestions registrationQuestions, File oktaPropsFile) throws IOException { try (ProgressBar progressBar = ProgressBar.create(true)) { progressBar.info("Check your email"); OrganizationResponse response = null; - while(response == null) { + while(response == null || !response.isActive()) { try { - // prompt for code - String code = registrationQuestions.getVerificationCode(); - response = organizationCreator.verifyNewOrg(identifier, code); - } catch (FactorVerificationException e) { - progressBar.info("Invalid Passcode, try again."); + // poll + Thread.sleep(verificationPollingFrequency.toMillis()); + response = organizationCreator.verifyNewOrg(identifier); + } catch (RestException | InterruptedException | ResourceException e) { + String error = "Failed to verify new Okta Organization. If you have already registered " + + "use \"okta login\" to configure the Okta CLI"; + progressBar.info(error); + throw new IllegalStateException(error, e); } } - // TODO handle polling in case the org is not ready sdkConfigurationService.writeOktaYaml(response.getOrgUrl(), response.getApiToken(), oktaPropsFile); progressBar.info("New Okta Account created!"); progressBar.info("Your Okta Domain: "+ response.getOrgUrl()); - progressBar.info("To set your password open this link:\n" + response.getUpdatePasswordUrl()); // TODO demo mode? } diff --git a/common/src/main/java/com/okta/cli/common/service/DefaultStartRestClient.java b/common/src/main/java/com/okta/cli/common/service/DefaultStartRestClient.java index 6a29cc32..b9df17de 100644 --- a/common/src/main/java/com/okta/cli/common/service/DefaultStartRestClient.java +++ b/common/src/main/java/com/okta/cli/common/service/DefaultStartRestClient.java @@ -18,10 +18,14 @@ import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.okta.cli.common.RestException; -import com.okta.cli.common.model.ErrorResponse; import com.okta.cli.common.model.SamplesListings; import com.okta.cli.common.model.VersionInfo; import com.okta.commons.lang.ApplicationInfo; +import com.okta.sdk.error.Error; +import com.okta.sdk.impl.ds.JacksonMapMarshaller; +import com.okta.sdk.impl.ds.MapMarshaller; +import com.okta.sdk.impl.error.DefaultError; +import com.okta.sdk.resource.ResourceException; import lombok.extern.slf4j.Slf4j; import org.apache.http.Header; import org.apache.http.HttpHeaders; @@ -35,18 +39,14 @@ import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; +import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; @Slf4j public class DefaultStartRestClient implements RestClient, StartRestClient { - /** - * The base URL of the service used to create a new Okta account. - * This value is NOT exposed as a plugin parameter, but CAN be set using the env var {@code OKTA_CLI_BASE_URL}. - */ - private static final String DEFAULT_API_BASE_URL = "https://start.okta.dev/"; - private static final String APPLICATION_JSON = "application/json"; private static final String USER_AGENT_STRING = ApplicationInfo.get().entrySet().stream() @@ -56,6 +56,18 @@ public class DefaultStartRestClient implements RestClient, StartRestClient { private final ObjectMapper objectMapper = new ObjectMapper() .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + private final MapMarshaller mapMarshaller = new JacksonMapMarshaller(); + + private final String baseUrl; + + public DefaultStartRestClient() { + this(Settings.getCliApiUrl()); + } + + public DefaultStartRestClient(String baseUrl) { + this.baseUrl = baseUrl; + } + @Override public VersionInfo getVersionInfo() throws IOException, RestException { return get("/versions/cli", VersionInfo.class); @@ -89,8 +101,7 @@ public T get(String url, Class responseType) throws RestException, IOExce return objectMapper.reader().readValue(content, responseType); } else { // assume error - ErrorResponse error = objectMapper.reader().readValue(content, ErrorResponse.class); - throw new RestException(error); + throw new ResourceException(error(content, response.getStatusLine().getStatusCode())); } } } @@ -124,20 +135,21 @@ public T post(String url, String body, Class responseType) throws RestExc return objectMapper.reader().readValue(content, responseType); } else { // assume error - ErrorResponse error = objectMapper.reader().readValue(content, ErrorResponse.class); - throw new RestException(error); + throw new ResourceException(error(content, response.getStatusLine().getStatusCode())); } } } private String fullUrl(String relative) { - return getApiBaseUrl() + relative; + return baseUrl + relative; } - private String getApiBaseUrl() { - // Resolve baseURL via ENV Var, System property, and fallback to the default - return System.getenv().getOrDefault("OKTA_CLI_BASE_URL", - System.getProperties().getProperty("okta.cli.baseUrl", - DEFAULT_API_BASE_URL)); + private Error error(InputStream content, int statusCode) { + Map data = mapMarshaller.unmarshal(content, Collections.emptyMap()); + DefaultError error = new DefaultError(data); + if (error.getStatus() < 0) { + error.setStatus(statusCode); + } + return error; } } diff --git a/common/src/main/java/com/okta/cli/common/service/OktaOrganizationCreator.java b/common/src/main/java/com/okta/cli/common/service/OktaOrganizationCreator.java index 58454d3e..b139a829 100644 --- a/common/src/main/java/com/okta/cli/common/service/OktaOrganizationCreator.java +++ b/common/src/main/java/com/okta/cli/common/service/OktaOrganizationCreator.java @@ -15,7 +15,6 @@ */ package com.okta.cli.common.service; -import com.okta.cli.common.FactorVerificationException; import com.okta.cli.common.RestException; import com.okta.cli.common.model.OrganizationRequest; import com.okta.cli.common.model.OrganizationResponse; @@ -26,5 +25,5 @@ public interface OktaOrganizationCreator { OrganizationResponse createNewOrg(OrganizationRequest orgRequest) throws IOException, RestException; - OrganizationResponse verifyNewOrg(String identifier, String code) throws FactorVerificationException, IOException; + OrganizationResponse verifyNewOrg(String identifier) throws IOException, RestException; } diff --git a/common/src/main/java/com/okta/cli/common/service/Settings.java b/common/src/main/java/com/okta/cli/common/service/Settings.java new file mode 100644 index 00000000..52ff0103 --- /dev/null +++ b/common/src/main/java/com/okta/cli/common/service/Settings.java @@ -0,0 +1,46 @@ +/* + * Copyright 2020-Present Okta, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.okta.cli.common.service; + +class Settings { + + /** + * The base URL of the service used to create a new Okta account. + * This value is NOT exposed as a plugin parameter, but CAN be set using the env var {@code OKTA_CLI_REGISTRATION_URL}. + */ + private static final String DEFAULT_CLI_API_URL = "https://start.okta.dev/"; + private static final String DEFAULT_REGISTRATION_BASE_URL = "https://okta-devok12.okta.com/"; + private static final String DEFAULT_REGISTRATION_ID = "reg405abrRAkn0TRf5d6"; + + static String getProperty(String envVar, String systemProperty, String defaultValue) { + // Resolve baseURL via ENV Var, System property, and fallback to the default + return System.getenv().getOrDefault(envVar, // check env var first + System.getProperties().getProperty(systemProperty, // try system property + defaultValue)); // fallback to default value + } + + static String getRegistrationBaseUrl() { + return getProperty("OKTA_CLI_REGISTRATION_URL", "okta.cli.registrationUrl", DEFAULT_REGISTRATION_BASE_URL); + } + + static String getRegistrationId() { + return getProperty("OKTA_CLI_REGISTRATION_ID", "okta.cli.registrationId", DEFAULT_REGISTRATION_ID); + } + + static String getCliApiUrl() { + return getProperty("OKTA_CLI_API_URL", "okta.cli.apiUrl", DEFAULT_CLI_API_URL); + } +} diff --git a/common/src/main/java/com/okta/cli/common/service/SetupService.java b/common/src/main/java/com/okta/cli/common/service/SetupService.java index a498c30a..a2814802 100644 --- a/common/src/main/java/com/okta/cli/common/service/SetupService.java +++ b/common/src/main/java/com/okta/cli/common/service/SetupService.java @@ -32,7 +32,7 @@ public interface SetupService { OrganizationResponse createOktaOrg(RegistrationQuestions registrationQuestions, File oktaPropsFile, boolean demo, - boolean interactive) throws IOException, ClientConfigurationException; + boolean interactive) throws IOException, ClientConfigurationException, UserCanceledException; void verifyOktaOrg(String identifier, RegistrationQuestions registrationQuestions, diff --git a/common/src/main/java/com/okta/cli/common/service/UserCanceledException.java b/common/src/main/java/com/okta/cli/common/service/UserCanceledException.java new file mode 100644 index 00000000..e12a7fe6 --- /dev/null +++ b/common/src/main/java/com/okta/cli/common/service/UserCanceledException.java @@ -0,0 +1,23 @@ +/* + * Copyright 2022-Present Okta, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.okta.cli.common.service; + +public class UserCanceledException extends Exception { + + public UserCanceledException() { + super(); + } +} diff --git a/common/src/test/groovy/com/okta/cli/common/service/DefaultOrganizationCreatorTest.groovy b/common/src/test/groovy/com/okta/cli/common/service/DefaultOrganizationCreatorTest.groovy index 38e1b09c..e2379b0b 100644 --- a/common/src/test/groovy/com/okta/cli/common/service/DefaultOrganizationCreatorTest.groovy +++ b/common/src/test/groovy/com/okta/cli/common/service/DefaultOrganizationCreatorTest.groovy @@ -33,13 +33,15 @@ class DefaultOrganizationCreatorTest implements WireMockSupport { @Override Collection wireMockStubMapping() { return [ - post("/create") + post("/api/v1/registration/reg405abrRAkn0TRf5d6/register") .withRequestBody(equalToJson(""" { - "firstName": "Joe", - "lastName": "Coder", - "email": "joe.coder@example.com", - "organization": "Test co" + "userProfile": { + "firstName": "Joe", + "lastName": "Coder", + "email": "joe.coder@example.com", + "country": "US of A" + } } """)) .withHeader("Content-Type", equalTo("application/json")) @@ -53,18 +55,17 @@ class DefaultOrganizationCreatorTest implements WireMockSupport { @Test void basicSuccessTest() { - RestoreEnvironmentVariables.setEnvironmentVariable("OKTA_CLI_BASE_URL", mockUrl()) + RestoreEnvironmentVariables.setEnvironmentVariable("OKTA_CLI_REGISTRATION_URL", mockUrl()) DefaultOktaOrganizationCreator creator = new DefaultOktaOrganizationCreator() OrganizationResponse response = creator.createNewOrg(new OrganizationRequest() .setEmail("joe.coder@example.com") - .setOrganization("Test co") + .setCountry("US of A") .setFirstName("Joe") .setLastName("Coder")) assertThat response.orgUrl, is("https://okta.example.com") assertThat response.apiToken, is("an-api-token-here") - assertThat response.email, is("joe.coder@example.com") } private String basicSuccess() { diff --git a/common/src/test/groovy/com/okta/cli/common/service/DefaultSetupServiceTest.groovy b/common/src/test/groovy/com/okta/cli/common/service/DefaultSetupServiceTest.groovy index ef71d2cd..2d4ebd61 100644 --- a/common/src/test/groovy/com/okta/cli/common/service/DefaultSetupServiceTest.groovy +++ b/common/src/test/groovy/com/okta/cli/common/service/DefaultSetupServiceTest.groovy @@ -15,9 +15,7 @@ */ package com.okta.cli.common.service -import com.okta.cli.common.FactorVerificationException import com.okta.cli.common.config.MutablePropertySource -import com.okta.cli.common.model.ErrorResponse import com.okta.cli.common.model.OidcProperties import com.okta.cli.common.model.OrganizationRequest import com.okta.cli.common.model.OrganizationResponse @@ -53,7 +51,7 @@ class DefaultSetupServiceTest { DefaultSetupService setupService = setupService() OrganizationRequest orgRequest = mock(OrganizationRequest) - RegistrationQuestions registrationQuestions = RegistrationQuestions.answers(true, orgRequest, null) + RegistrationQuestions registrationQuestions = RegistrationQuestions.answers(true, orgRequest) File oktaPropsFile = mock(File) OrganizationResponse orgResponse = mock(OrganizationResponse) when(setupService.organizationCreator.createNewOrg(orgRequest)).thenReturn(orgResponse) @@ -69,21 +67,21 @@ class DefaultSetupServiceTest { String newOrgUrl = "https://org.example.com" DefaultSetupService setupService = setupService() - RegistrationQuestions registrationQuestions = RegistrationQuestions.answers(true, null, "123456") + RegistrationQuestions registrationQuestions = RegistrationQuestions.answers(true, null) File oktaPropsFile = mock(File) OrganizationResponse orgResponse = mock(OrganizationResponse) - when(setupService.organizationCreator.verifyNewOrg("test-id", "123456")).thenReturn(orgResponse) + when(setupService.organizationCreator.verifyNewOrg("test-id")).thenReturn(orgResponse) + when(orgResponse.isActive()).thenReturn(true) when(orgResponse.getOrgUrl()).thenReturn(newOrgUrl) - when(orgResponse.getUpdatePasswordUrl()).thenReturn("https://reset.password") setupService.verifyOktaOrg("test-id", registrationQuestions, oktaPropsFile) - verify(setupService.organizationCreator).verifyNewOrg("test-id", "123456") + verify(setupService.organizationCreator).verifyNewOrg("test-id") } @Test - void verifyOktaOrg_invalidCode() { + void verifyOktaOrg_pendingStatus() { String newOrgUrl = "https://org.example.com" DefaultSetupService setupService = setupService() @@ -91,20 +89,13 @@ class DefaultSetupServiceTest { File oktaPropsFile = mock(File) OrganizationResponse orgResponse = mock(OrganizationResponse) RegistrationQuestions registrationQuestions = mock(RegistrationQuestions) - when(registrationQuestions.getVerificationCode()).thenReturn("123456").thenReturn("654321") - when(setupService.organizationCreator.verifyNewOrg("test-id", "123456")).thenThrow(new FactorVerificationException(new ErrorResponse() - .setStatus(401) - .setError("test-error") - .setMessage("test-message") - .setCauses(["one", "two"]) - , new Throwable("root-test-cause"))) - when(setupService.organizationCreator.verifyNewOrg("test-id", "654321")).thenReturn(orgResponse) + when(setupService.organizationCreator.verifyNewOrg("test-id")).thenReturn(orgResponse) + when(orgResponse.isActive()).thenReturn(false, true) when(orgResponse.getOrgUrl()).thenReturn(newOrgUrl) - when(orgResponse.getUpdatePasswordUrl()).thenReturn("https://reset.password") setupService.verifyOktaOrg("test-id", registrationQuestions, oktaPropsFile) - verify(setupService.organizationCreator).verifyNewOrg("test-id", "123456") + verify(setupService.organizationCreator, times(2)).verifyNewOrg("test-id") } @Test diff --git a/integration-tests/src/test/groovy/com/okta/cli/test/AppsCreateIT.groovy b/integration-tests/src/test/groovy/com/okta/cli/test/AppsCreateIT.groovy index 94b9ced5..ad4e5274 100644 --- a/integration-tests/src/test/groovy/com/okta/cli/test/AppsCreateIT.groovy +++ b/integration-tests/src/test/groovy/com/okta/cli/test/AppsCreateIT.groovy @@ -373,7 +373,7 @@ class AppsCreateIT implements MockWebSupport, CreateAppSupport { def result = new CommandRunner() .withSdkConfig(url(mockWebServer,"/")) - .runCommandWithInput(input,"--verbose", "--color=never", "apps", "create") + .runCommandWithInput(input,"--color=never", "apps", "create") assertThat result, resultMatches(0, allOf( containsString("Created OIDC application, client-id: test-id"), diff --git a/integration-tests/src/test/groovy/com/okta/cli/test/AppsDeleteIT.groovy b/integration-tests/src/test/groovy/com/okta/cli/test/AppsDeleteIT.groovy index 77afb122..d14692a7 100644 --- a/integration-tests/src/test/groovy/com/okta/cli/test/AppsDeleteIT.groovy +++ b/integration-tests/src/test/groovy/com/okta/cli/test/AppsDeleteIT.groovy @@ -46,7 +46,7 @@ class AppsDeleteIT implements MockWebSupport, CreateAppSupport { def result = new CommandRunner() .withSdkConfig(mockWebServer.url("/").toString()) - .runCommandWithInput(input, "--color=never", "apps", "delete", "--verbose", "app-id1") + .runCommandWithInput(input, "--color=never", "apps", "delete", "app-id1") assertThat result, resultMatches(0, allOf( containsString("Deactivate and delete application 'app-id1'? [y/N]"), @@ -107,7 +107,7 @@ class AppsDeleteIT implements MockWebSupport, CreateAppSupport { def result = new CommandRunner() .withSdkConfig(mockWebServer.url("/").toString()) - .runCommandWithInput(input, "--color=never", "apps", "delete", "--verbose", "app-id1") + .runCommandWithInput(input, "--color=never", "apps", "delete", "app-id1") assertThat result, resultMatches(0, allOf( containsString("Deactivate and delete application 'app-id1'? [y/N]"), @@ -134,7 +134,7 @@ class AppsDeleteIT implements MockWebSupport, CreateAppSupport { def result = new CommandRunner() .withSdkConfig(mockWebServer.url("/").toString()) - .runCommandWithInput(input, "--color=never", "apps", "delete", "--verbose", "app-id1") + .runCommandWithInput(input, "--color=never", "apps", "delete", "app-id1") assertThat result, resultMatches(1, containsString("Application 'app-id1' has already been marked for deletion"), null) @@ -161,7 +161,7 @@ class AppsDeleteIT implements MockWebSupport, CreateAppSupport { def result = new CommandRunner() .withSdkConfig(mockWebServer.url("/").toString()) - .runCommandWithInput(input, "--color=never", "apps", "delete", "--verbose", "app-id1") + .runCommandWithInput(input, "--color=never", "apps", "delete", "app-id1") assertThat result, resultMatches(1, allOf( containsString("Deactivate and delete application 'app-id1'? [y/N]")), @@ -219,7 +219,7 @@ class AppsDeleteIT implements MockWebSupport, CreateAppSupport { def result = new CommandRunner() .withSdkConfig(mockWebServer.url("/").toString()) - .runCommandWithInput(input, "--color=never", "apps", "delete", "--verbose", "app-id1", "app-id2") + .runCommandWithInput(input, "--color=never", "apps", "delete", "app-id1", "app-id2") assertThat result, resultMatches(1, allOf( containsString("Deactivate and delete application 'app-id1'? [y/N]"), diff --git a/integration-tests/src/test/groovy/com/okta/cli/test/CommandRunner.groovy b/integration-tests/src/test/groovy/com/okta/cli/test/CommandRunner.groovy index e4dc682d..c8ed73d1 100644 --- a/integration-tests/src/test/groovy/com/okta/cli/test/CommandRunner.groovy +++ b/integration-tests/src/test/groovy/com/okta/cli/test/CommandRunner.groovy @@ -82,7 +82,7 @@ class CommandRunner { String homeDirString = escapePath(homeDir.absolutePath) - List command = [getCli(homeDir), "-Duser.home=${homeDirString}", "-Dokta.testing.disableHttpsCheck=true", "-Dokta.cli.baseUrl=${regServiceUrl}"] + List command = [getCli(homeDir), "-Duser.home=${homeDirString}", "-Dokta.testing.disableHttpsCheck=true", "-Dokta.cli.registrationUrl=${regServiceUrl}", "-Dokta.cli.apiUrl=${regServiceUrl}"] command.addAll(args) String cmd = command.join(" ") diff --git a/integration-tests/src/test/groovy/com/okta/cli/test/CreateAppSupport.groovy b/integration-tests/src/test/groovy/com/okta/cli/test/CreateAppSupport.groovy index 1326bb1f..e442f6ce 100644 --- a/integration-tests/src/test/groovy/com/okta/cli/test/CreateAppSupport.groovy +++ b/integration-tests/src/test/groovy/com/okta/cli/test/CreateAppSupport.groovy @@ -46,7 +46,7 @@ trait CreateAppSupport { ] } - void verifyOrgCreateRequest(RecordedRequest request, String firstName = "test-first", String lastName = "test-last", String email = "test-email@example.com", String company = "test co") { + void verifyOrgCreateRequest(RecordedRequest request, String firstName = "test-first", String lastName = "test-last", String email = "test-email@example.com", String country = "Petoria") { assertThat request.getRequestLine(), equalTo("POST /create HTTP/1.1") assertThat request.getHeader("Content-Type"), equalTo("application/json") Map body = new JsonSlurper().parse(request.getBody().readByteArray(), StandardCharsets.UTF_8.toString()) @@ -54,7 +54,7 @@ trait CreateAppSupport { firstName: firstName, lastName: lastName, email: email, - organization: company + country: country ]) } diff --git a/integration-tests/src/test/groovy/com/okta/cli/test/RegisterIT.groovy b/integration-tests/src/test/groovy/com/okta/cli/test/RegisterIT.groovy index 0fbe69b6..2fdf500c 100644 --- a/integration-tests/src/test/groovy/com/okta/cli/test/RegisterIT.groovy +++ b/integration-tests/src/test/groovy/com/okta/cli/test/RegisterIT.groovy @@ -15,7 +15,6 @@ */ package com.okta.cli.test -import com.okta.cli.common.model.ErrorResponse import groovy.json.JsonSlurper import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer @@ -34,8 +33,8 @@ class RegisterIT implements MockWebSupport { void happyPath() { List responses = [ - jsonRequest('{ "orgUrl": "https://result.example.com", "email": "test-email@example.com", "id": "test-id" }'), - jsonRequest('{ "orgUrl": "https://result.example.com", "email": "test-email@example.com", "apiToken": "fake-test-token" }') + jsonRequest('{ "developerOrgCliToken": "test-id" }'), + jsonRequest('{ "orgUrl": "https://result.example.com", "email": "test-email@example.com", "apiToken": "fake-test-token", "status": "ACTIVE" }') ] MockWebServer mockWebServer = createMockServer() @@ -46,23 +45,25 @@ class RegisterIT implements MockWebSupport { "test-first", "test-last", "test-email@example.com", - "test co", - "123456" + "Petoria", ] - def result = new CommandRunner(url(mockWebServer, "/")).runCommandWithInput(input, "register", "--verbose") - assertThat result, resultMatches(0, allOf(containsString("An email has been sent to you with a verification code."), containsString("Verification code")), emptyString()) + def result = new CommandRunner(url(mockWebServer, "/")).runCommandWithInput(input, "register") + assertThat result, resultMatches(0, allOf(containsString("An account activation email has been sent to you.")), emptyString()) RecordedRequest request = mockWebServer.takeRequest() - assertThat request.getRequestLine(), equalTo("POST /create HTTP/1.1") + assertThat request.getRequestLine(), equalTo("POST /api/v1/registration/reg405abrRAkn0TRf5d6/register HTTP/1.1") assertThat request.getHeader("Content-Type"), is("application/json") Map body = new JsonSlurper().parse(request.getBody().readByteArray(), StandardCharsets.UTF_8.toString()) assertThat body, equalTo([ + userProfile: [ firstName: "test-first", lastName: "test-last", email: "test-email@example.com", - organization: "test co" + country: "Petoria", + okta_oie: true + ] ]) File oktaConfigFile = new File(result.homeDir, ".okta/okta.yaml") @@ -74,8 +75,8 @@ class RegisterIT implements MockWebSupport { void existingConfigFile_overwrite() { List responses = [ - jsonRequest('{ "orgUrl": "https://result.example.com", "email": "test-email@example.com", "id": "test-id" }'), - jsonRequest('{ "orgUrl": "https://result.example.com", "email": "test-email@example.com", "apiToken": "fake-test-token" }') + jsonRequest('{ "developerOrgCliToken": "test-id" }'), + jsonRequest('{ "orgUrl": "https://result.example.com", "email": "test-email@example.com", "apiToken": "fake-test-token", "status": "ACTIVE" }\') }') ] MockWebServer mockWebServer = createMockServer() @@ -87,34 +88,43 @@ class RegisterIT implements MockWebSupport { "test-first", "test-last", "test-email@example.com", - "test co", - "123456" + "Petoria" ] CommandRunner runner = new CommandRunner(url(mockWebServer, "/")) .withHomeDirectory { File oktaYaml = new File(it, ".okta/okta.yaml") oktaYaml.getParentFile().mkdirs() - oktaYaml.write(""" -okta: - client: - orgUrl: https://test.example.com - token: test-token -""") + oktaYaml.write( + """\ + okta: + client: + orgUrl: https://test.example.com + token: test-token + """.stripIndent()) } def result = runner.runCommandWithInput(input, "register") - assertThat result, resultMatches(0, allOf(containsString("An email has been sent to you with a verification code."), containsString("Verification code")), emptyString()) + assertThat result, resultMatches(0, allOf( + containsString("Creating new Okta Organization, this may take a minute:"), + containsString("An account activation email has been sent to you."), + containsString("Check your email"), + containsString("New Okta Account created!"), + containsString("Your Okta Domain: https://result.example.com") + ), emptyString()) RecordedRequest request = mockWebServer.takeRequest() - assertThat request.getRequestLine(), equalTo("POST /create HTTP/1.1") + assertThat request.getRequestLine(), equalTo("POST /api/v1/registration/reg405abrRAkn0TRf5d6/register HTTP/1.1") assertThat request.getHeader("Content-Type"), is("application/json") Map body = new JsonSlurper().parse(request.getBody().readByteArray(), StandardCharsets.UTF_8.toString()) assertThat body, equalTo([ + userProfile: [ firstName: "test-first", lastName: "test-last", email: "test-email@example.com", - organization: "test co" + country: "Petoria", + okta_oie: true + ] ]) File oktaConfigFile = new File(result.homeDir, ".okta/okta.yaml") @@ -133,16 +143,17 @@ okta: .withHomeDirectory { File oktaYaml = new File(it, ".okta/okta.yaml") oktaYaml.getParentFile().mkdirs() - oktaYaml.write(""" -okta: - client: - orgUrl: https://test.example.com - token: test-token -""") + oktaYaml.write( + """\ + okta: + client: + orgUrl: https://test.example.com + token: test-token + """.stripIndent()) } def result = runner.runCommandWithInput(input, "register") - assertThat result, resultMatches(1, containsString("Overwrite configuration file?"), containsString("User canceled")) + assertThat result, resultMatches(2, containsString("Overwrite configuration file?"), emptyString()) File oktaConfigFile = new File(result.homeDir, ".okta/okta.yaml") assertThat oktaConfigFile, new OktaConfigMatcher("https://test.example.com", "test-token") @@ -150,17 +161,11 @@ okta: } @Test - void invalidCodeTest() { + void pollingTest() { List responses = [ - jsonRequest('{ "orgUrl": "https://result.example.com", "email": "test-email@example.com", "id": "test-id" }'), - jsonRequest(new ErrorResponse() - .setError("Invalid passcode") - .setMessage("Test message") - .setCauses(["error 1", "error 2"]) - .setStatus(401), - 401 - ), - jsonRequest('{ "orgUrl": "https://result.example.com", "email": "test-email@example.com", "apiToken": "fake-test-token" }') + jsonRequest('{ "developerOrgCliToken": "test-id" }'), + jsonRequest('{ "status": "PENDING" }'), + jsonRequest('{ "orgUrl": "https://result.example.com", "email": "test-email@example.com", "apiToken": "fake-test-token", "status": "ACTIVE" }') ] MockWebServer mockWebServer = createMockServer() @@ -171,23 +176,24 @@ okta: "test-first", "test-last", "test-email@example.com", - "test co", - "123456", - "654321" + "Petoria" ] def result = new CommandRunner(url(mockWebServer, "/")).runCommandWithInput(input, "register") - assertThat result, resultMatches(0, allOf(containsString("An email has been sent to you with a verification code."), containsString("Verification code")), emptyString()) + assertThat result, resultMatches(0, containsString("An account activation email has been sent to you."), emptyString()) RecordedRequest request = mockWebServer.takeRequest() - assertThat request.getRequestLine(), equalTo("POST /create HTTP/1.1") + assertThat request.getRequestLine(), equalTo("POST /api/v1/registration/reg405abrRAkn0TRf5d6/register HTTP/1.1") assertThat request.getHeader("Content-Type"), is("application/json") Map body = new JsonSlurper().parse(request.getBody().readByteArray(), StandardCharsets.UTF_8.toString()) assertThat body, equalTo([ + userProfile: [ firstName: "test-first", lastName: "test-last", email: "test-email@example.com", - organization: "test co" + country: "Petoria", + okta_oie: true + ] ]) File oktaConfigFile = new File(result.homeDir, ".okta/okta.yaml") diff --git a/integration-tests/src/test/groovy/com/okta/cli/test/StartIT.groovy b/integration-tests/src/test/groovy/com/okta/cli/test/StartIT.groovy index 46148939..ff393742 100644 --- a/integration-tests/src/test/groovy/com/okta/cli/test/StartIT.groovy +++ b/integration-tests/src/test/groovy/com/okta/cli/test/StartIT.groovy @@ -48,8 +48,8 @@ class StartIT implements MockWebSupport, CreateAppSupport { List responses = [ // reg requests - jsonRequest('{ "orgUrl": "' + orgUrl + '", "email": "test-email@example.com", "id": "test-id" }'), - jsonRequest('{ "orgUrl": "' + orgUrl + '", "email": "test-email@example.com", "apiToken": "fake-test-token", "updatePasswordUrl": "' + orgUrl + 'password-reset-link"}'), + jsonRequest('{ "orgUrl": "' + orgUrl + '", "email": "test-email@example.com", "developerOrgCliToken": "test-id" }'), + jsonRequest('{ "orgUrl": "' + orgUrl + '", "email": "test-email@example.com", "apiToken": "fake-test-token", "status": "ACTIVE" }'), // list samples jsonRequest([items: [ @@ -86,9 +86,8 @@ class StartIT implements MockWebSupport, CreateAppSupport { "test-first", "test-last", "test-email@example.com", - "test co", - "123456", - "", // accept prompt to change password + "Petoria", + "", // accept prompt to continue // select a sample "2" ] @@ -96,8 +95,7 @@ class StartIT implements MockWebSupport, CreateAppSupport { def result = new CommandRunner(url(mockWebServer, "/")).runCommandWithInput(input, "start") assertThat result, resultMatches(0, allOf( // registration - containsString("An email has been sent to you with a verification code."), - containsString("Verification code"), + containsString("An account activation email has been sent to you."), containsString("Select a sample"), containsString("a test description"), containsString("Change the directory:"),