Skip to content
Merged
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
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ dependencies {
implementation("io.micronaut:micronaut-http-client")
implementation("io.micronaut.picocli:micronaut-picocli")
implementation("io.micronaut.serde:micronaut-serde-jackson")
implementation("io.micronaut:micronaut-retry")
testImplementation("net.datafaker:datafaker:2.5.1")
compileOnly("org.projectlombok:lombok")
runtimeOnly("ch.qos.logback:logback-classic")
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/org/justserve/cli/JustServeCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
import io.micronaut.configuration.picocli.PicocliRunner;
import org.justserve.cli.command.BaseCommand;
import org.justserve.cli.command.GetTempPassword;
import org.justserve.cli.command.MakeOrgAdmin;
import org.justserve.cli.util.JustServeVersionProvider;
import picocli.CommandLine.Command;
import picocli.CommandLine.ParameterException;
import picocli.jansi.graalvm.AnsiConsole;

@Command(subcommands = GetTempPassword.class,
@Command(subcommands = {GetTempPassword.class, MakeOrgAdmin.class},
mixinStandardHelpOptions = true,
name = "justserve", versionProvider = JustServeVersionProvider.class,
description = "justserve-cli is a terminal tool to help specialists and admin using JustServe")
Expand Down
16 changes: 15 additions & 1 deletion src/main/java/org/justserve/cli/command/BaseCommand.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.justserve.cli.command;

import io.micronaut.context.annotation.Value;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.ReflectiveAccess;
import picocli.CommandLine.Command;
Expand All @@ -9,15 +10,28 @@
import java.io.PrintWriter;
import java.util.Optional;

import static org.justserve.cli.util.JustServePrinter.printError;
import static picocli.CommandLine.Help.Ansi.AUTO;

@Command
public class BaseCommand implements ConsoleOutput{
public class BaseCommand implements ConsoleOutput {

@Spec
@ReflectiveAccess
protected CommandSpec spec;

@Value("${justserve.token}")
String token;

boolean validateToken() {
if ("i-need-to-be-defined".equals(token) || null == token) {
printError(("NO AUTHENTICATION PROVIDED" + System.lineSeparator() +
"The Authentication token is not assigned as an environment variable." + System.lineSeparator() +
"Please define the environment variable \"JUSTSERVE_TOKEN\" and try again."));
return false;
}
return true;
}

@Override
public void out(String message) {
Expand Down
15 changes: 4 additions & 11 deletions src/main/java/org/justserve/cli/command/GetTempPassword.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
package org.justserve.cli.command;

import io.micronaut.context.annotation.Value;
import io.micronaut.core.annotation.ReflectiveAccess;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.client.exceptions.HttpClientResponseException;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import jakarta.validation.constraints.Email;
import org.justserve.client.UserClient;
import org.justserve.model.UserHashRequestByEmail;
import picocli.CommandLine.Command;
Expand All @@ -17,26 +16,20 @@
@Command(name = "getTempPassword", description = "get a temporary password for a user")
public class GetTempPassword extends BaseCommand implements Runnable {

@ReflectiveAccess
@Email
@Option(names = {"-e", "--email"}, description = "email for the user whose temporary password will be generated")
String email;

@Inject
@ReflectiveAccess
Provider<UserClient> userClientProvider;

@Value("${justserve.token}")
String token;

@Override
public void run() {
HttpResponse<String> response;
if ("i-need-to-be-defined".equals(token) || null == token) {
printError(("NO AUTHENTICATION PROVIDED" + System.lineSeparator() +
"The Authentication token is not assigned as an environment variable." + System.lineSeparator() +
"Please define the environment variable \"JUSTSERVE_TOKEN\" and try again."));
if (!validateToken()) {
return;
}
HttpResponse<String> response;
try {
UserClient userClient = userClientProvider.get();
response = userClient.getTempPassword(new UserHashRequestByEmail(email));
Expand Down
121 changes: 121 additions & 0 deletions src/main/java/org/justserve/cli/command/MakeOrgAdmin.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package org.justserve.cli.command;

import io.micronaut.core.annotation.Nullable;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.client.exceptions.HttpClientResponseException;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j;
import org.justserve.client.BoundaryPermissionClient;
import org.justserve.client.DynamicRoutingClient;
import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;

import java.util.*;
import java.util.stream.Collectors;

import static org.justserve.cli.util.JustServePrinter.printError;
import static org.justserve.cli.util.JustServePrinter.printNormal;

@Slf4j
@Command(name = "makeOrgAdmin", description = "make a user an admin for the provided organization(s). " +
"Makes no changes to the user's boundaries.", mixinStandardHelpOptions = true)
public class MakeOrgAdmin extends BaseCommand implements Runnable {

@Option(names = {"--user", "-u"}, description = "the user who will be made org admin")
private UUID user;

@Option(names = {"-o", "--org"}, description = "The organization(s) specified by URL slug or UID.",
converter = OrgConverter.class, arity = "1..*", split = ",")
private Org[] orgs;

@Inject
Provider<DynamicRoutingClient> dynamicRoutingClientProvider;

@Inject
Provider<BoundaryPermissionClient> boundaryPermissionClientProvider;

@Override
public void run() {
if (!validateToken()) {
return;
}
DynamicRoutingClient dynamicRoutingClient = dynamicRoutingClientProvider.get();
// since we allow submitting orgId's and orgUrl, convert any slugs to orgId's
Map<Org, @Nullable UUID> orgUuidMap = Arrays.stream(orgs).distinct().parallel()
.collect(Collectors.toMap(
org -> org,
org -> org instanceof OrgId ? ((OrgId) org).getId() : dynamicRoutingClient
.getOrgIdFromSlug(((OrgSlug) org).getSlug()).body().getId()
));
if (orgUuidMap.values().stream().anyMatch(Objects::isNull)) {
// provide a list of all invalid org slugs, not just the first failure
List<OrgSlug> invalidOrgSlugs = new ArrayList<>();
orgUuidMap.entrySet().stream()
.filter(entry -> entry.getValue() == null)
.map(entry -> (OrgSlug) entry.getKey())
.forEach(invalidOrgSlugs::add);
printError("The following organization slugs are invalid: " + invalidOrgSlugs.stream()
.map(OrgSlug::getSlug)
.collect(Collectors.joining(", ")));
return;
}
log.atTrace().log("Finished converting any slugs to org id's.");
BoundaryPermissionClient boundaryPermissionClient = boundaryPermissionClientProvider.get();
Map<Org, @Nullable UUID> successfulReassignments = new HashMap<>();
orgUuidMap.entrySet().stream().parallel().forEach(entry -> {
Org org = entry.getKey();
UUID orgId = entry.getValue();
String orgIdentifier = org instanceof OrgSlug ? ((OrgSlug) org).getSlug() + " (" + orgId + ")" : orgId.toString();

log.atTrace().log("Making user {} an admin for org {}", user, orgIdentifier);
try {
log.atTrace().log("sending request to make user {} an admin for org {}.", user, orgIdentifier);
HttpResponse<Object> response = boundaryPermissionClient.makeAdminForOrg(orgId, user);
log.atTrace().log("received api response status: {}", response.status());
log.atDebug().log("Successfully made user {} an admin for org {}.", user, orgIdentifier);
successfulReassignments.put(org, orgId);
} catch (HttpClientResponseException e) {
printError("Failed to make user " + user + " an admin for organization " + orgIdentifier + ".");
log.atError().setCause(e).log("Error response from API: {}", e.getResponse().body());
}
});
printNormal("successfully reassigned %d orgs to user %s", successfulReassignments.size(), user);
}

/**
* abstract class whose sole purpose is to allow the user to pass a list of orgID's and org Slugs
*/
private abstract static class Org {
}

@EqualsAndHashCode(callSuper = true)
@Data
private static class OrgSlug extends Org {
private final String slug;
}

@EqualsAndHashCode(callSuper = true)
@Data
private static class OrgId extends Org {
private final UUID id;
}

private static class OrgConverter implements CommandLine.ITypeConverter<Org> {
@Override
public Org convert(String value) {
try {
return new OrgId(UUID.fromString(value));
} catch (IllegalArgumentException e) {
if (value.matches("[^\\s/]+")) {
return new OrgSlug(value);
} else {
throw new CommandLine.TypeConversionException("'" + value + "' is not a valid organization UID or URL slug.");
}
}
}
}
}
11 changes: 10 additions & 1 deletion src/main/java/org/justserve/cli/util/JustServePrinter.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,22 @@ public static void printNormal(String message) {
jsPrint(message, normalStyle);
}

/**
* Prints a standard message using the primary brand color (JustServe Blue).
*
* @param message The message to print.
*/
public static void printNormal(String message, Object... args) {
jsPrint(String.format(message, args), normalStyle);
}

/**
* Returns a String stylized in Orange.
*
* @param message The message to print.
*/
public static String styleTitle(String message) {
return applyStyle(message, titleStyle);
return applyStyle(message, titleStyle);
}

/**
Expand Down
33 changes: 33 additions & 0 deletions src/main/java/org/justserve/client/BoundaryPermissionClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package org.justserve.client;

import io.micronaut.http.HttpResponse;
import io.micronaut.http.annotation.PathVariable;
import io.micronaut.http.annotation.Put;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.retry.annotation.Retryable;
import jakarta.validation.constraints.NotNull;

import java.util.UUID;

@Client("justserve")
public interface BoundaryPermissionClient {

/**
* Make User an admin to a given organization. returns a null response. Makes no changes to a user&#39;s boundaries,
* all boundary changes to be handled outside of this api call
*
* @since 0.0.7
* @author Jonathan Zollinger
*
* @param orgId (required)
* @param userId (required)
* @return OK (status code 200)
*/
@Retryable
@Put("/api/v1/boundaries/rep/{userId}/org/{orgId}")
HttpResponse<Object> makeAdminForOrg(
@PathVariable("orgId") @NotNull UUID orgId,
@PathVariable("userId") @NotNull UUID userId
);

}
40 changes: 0 additions & 40 deletions src/main/resources/schema.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,51 +6,11 @@ info:
name: Jonathan Zollinger
version: v0.0.5
tags:
- BoundaryPermission
- DynamicRouting
- Image
- User
- Organization
paths:
/api/v1/boundaries/rep/{userId}/org/{orgId}:
put:
tags: [ BoundaryPermission ]
operationId: MakeAdminForOrg
description: Make User an admin to a given organization. returns a null response.
parameters:
- name: orgId
in: path
required: true
schema: { type: string, format: uuid }
- name: userId
in: path
required: true
schema: { type: string, format: uuid }
responses:
'200':
description: OK
content:
application/json:
schema:
type: null
/api/v1/boundaries/update:
put:
tags:
- BoundaryPermission
operationId: updateBoundary
description: Modify a user's boundaries
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/BoundaryUpdateRequest'
responses:
'200':
description: OK
content:
application/json:
schema:
type: null
/api/v1/images:
post:
tags: [ Image ]
Expand Down
30 changes: 27 additions & 3 deletions src/test/groovy/org/justserve/JustServeSpec.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import io.micronaut.context.ApplicationContext
import io.micronaut.context.env.Environment
import io.micronaut.http.HttpResponse
import io.micronaut.http.HttpStatus
import io.micronaut.http.client.exceptions.HttpClientResponseException
import io.micronaut.test.extensions.spock.annotation.MicronautTest
import net.datafaker.Faker
import org.justserve.client.*
Expand Down Expand Up @@ -80,7 +81,16 @@ class JustServeSpec extends Specification {
}

def createUser() {
return createUser(new TestUser(new Faker(Locale.of("en-us"))))
def response
while (null == response) {
try {
// A new user is generated on each loop iteration to avoid collisions
response = createUser(noAuthUserClient, new TestUser(new Faker(Locale.of("en-us"))))
} catch (HttpClientResponseException ignored) {
// This user likely already exists, so we'll loop and try a new one.
}
}
return response
}

def createUser(UserClient client = noAuthUserClient, TestUser user) {
Expand All @@ -92,10 +102,13 @@ class JustServeSpec extends Specification {
user.zipcode,
user.locale,
user.country,
user.countryCode
)
user.countryCode)
}

/**
* creates a random org for testing
* @return
*/
UUID createOrg() {
def orgRequest = new OrganizationCreateRequest()
.setContactEmail(faker.internet().emailAddress())
Expand All @@ -113,6 +126,17 @@ class JustServeSpec extends Specification {
return authDynamicRoutingClient.getOrgIdFromSlug(orgRequest.url).body().id
}

/**
* Creates a specified number of random organizations for testing.
* @param count The number of organizations to create.
* @return A list of UUIDs for the created organizations.
*/
List<UUID> createOrgs(int count) {
return (1..count).collect {
createOrg()
}
}


/**
* Creates a default organization search request for testing.
Expand Down
Loading
Loading