diff --git a/Dockerfile b/Dockerfile index a3ac6a7..2345903 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,7 +16,6 @@ USER 2021:2020 COPY target/dependency /usr/lib/web-service/lib COPY target/${JAR_FILE} /usr/lib/web-service/app.jar -COPY _config.yml /etc/artipie/artipie.yml WORKDIR /var/web-service HEALTHCHECK --interval=10s --timeout=3s \ @@ -28,7 +27,5 @@ CMD [ \ "--add-opens", "java.base/java.util=ALL-UNNAMED", \ "--add-opens", "java.base/java.security=ALL-UNNAMED", \ "-cp", "/usr/lib/web-service/app.jar:/usr/lib/web-service/lib/*", \ - "com.artipie.front.Service", \ - "--port=8080", \ - "--config=/etc/artipie/artipie.yml" \ + "com.artipie.front.Service" \ ] diff --git a/_config.yml b/_config.yml deleted file mode 100644 index 296b2df..0000000 --- a/_config.yml +++ /dev/null @@ -1,14 +0,0 @@ -# Default config for Artipie docker image -meta: - storage: - type: fs - path: /var/artipie/repo - credentials: - - - type: env - - - type: github - - - type: file - path: _credentials.yml - layout: org diff --git a/pom.xml b/pom.xml index eb305a5..2e2ee79 100644 --- a/pom.xml +++ b/pom.xml @@ -152,21 +152,6 @@ https://github.com/artipie/front/LICENSE.txt commons-cli 1.5.0 - - software.amazon.awssdk - auth - 2.14.7 - - - software.amazon.awssdk - s3 - 2.14.7 - - - io.etcd - jetcd-core - 0.5.4 - com.fasterxml.jackson.core jackson-databind diff --git a/src/main/java/com/artipie/front/AuthFilters.java b/src/main/java/com/artipie/front/AuthFilters.java index 83a8e39..7b7062e 100644 --- a/src/main/java/com/artipie/front/AuthFilters.java +++ b/src/main/java/com/artipie/front/AuthFilters.java @@ -23,7 +23,7 @@ public enum AuthFilters implements Filter { */ AUTHENTICATE( (req, rsp) -> { - if (req.pathInfo().equals("/signin") || req.pathInfo().equals("/token")) { + if ("/signin".equals(req.pathInfo()) || "/.health".equals(req.pathInfo())) { return; } if (req.session() == null || !req.session().attributes().contains("uid")) { @@ -43,16 +43,17 @@ public enum AuthFilters implements Filter { ); if (req.session() == null) { attrs.values().forEach(attr -> attr.remove(req)); - } - attrs.forEach( - (name, attr) -> { - if (req.session().attributes().contains(name)) { - attr.write(req, req.session().attribute(name)); - } else { - attr.remove(req); + } else { + attrs.forEach( + (name, attr) -> { + if (req.session().attributes().contains(name)) { + attr.write(req, req.session().attribute(name)); + } else { + attr.remove(req); + } } - } - ); + ); + } } ); @@ -71,8 +72,6 @@ public enum AuthFilters implements Filter { @Override public void handle(final Request req, final Response rsp) throws Exception { - if (!req.pathInfo().startsWith("/api")) { - this.func.handle(req, rsp); - } + this.func.handle(req, rsp); } } diff --git a/src/main/java/com/artipie/front/Layout.java b/src/main/java/com/artipie/front/Layout.java new file mode 100644 index 0000000..8e5ee19 --- /dev/null +++ b/src/main/java/com/artipie/front/Layout.java @@ -0,0 +1,42 @@ +/* + * The MIT License (MIT) Copyright (c) 2022 artipie.com + * https://github.com/artipie/front/LICENSE.txt + */ +package com.artipie.front; + +/** + * Repository layout. + * + * @since 1.0 + */ +public enum Layout { + /** + * Flat layout. + */ + FLAT("flat"), + /** + * Org layout. + */ + ORG("org"); + + /** + * Name of layout. + */ + private final String name; + + /** + * Ctor. + * @param name Name of layout. + */ + Layout(final String name) { + this.name = name; + } + + /** + * The name of the layout. + * @return String name + */ + public String toString() { + return this.name; + } +} diff --git a/src/main/java/com/artipie/front/RestException.java b/src/main/java/com/artipie/front/RestException.java new file mode 100644 index 0000000..5818078 --- /dev/null +++ b/src/main/java/com/artipie/front/RestException.java @@ -0,0 +1,64 @@ +/* + * The MIT License (MIT) Copyright (c) 2022 artipie.com + * https://github.com/artipie/front/LICENSE.txt + */ +package com.artipie.front; + +import com.artipie.ArtipieException; + +/** + * Exception should be used in wrong result of rest-invocation. + * + * @since 1.0 + * @implNote RestException is unchecked exception, but it's a good + * practice to document it via {@code throws} tag in JavaDocs. + */ +@SuppressWarnings("PMD.OnlyOneConstructorShouldDoInitialization") +public class RestException extends ArtipieException { + private static final long serialVersionUID = 1L; + + /** + * Status code. + */ + private final int code; + + /** + * New exception with message and base cause. + * @param code Http status code + * @param msg Message + * @param cause Cause + */ + public RestException(final int code, final String msg, final Throwable cause) { + super(msg, cause); + this.code = code; + } + + /** + * New exception with base cause. + * @param code Http status code + * @param cause Cause + */ + public RestException(final int code, final Throwable cause) { + super(cause); + this.code = code; + } + + /** + * New exception with message. + * @param code Http status code + * @param msg Message + */ + public RestException(final int code, final String msg) { + super(msg); + this.code = code; + } + + /** + * Get http status code of rest invocation's result . + * @return Status code. + */ + public int statusCode() { + return this.code; + } +} + diff --git a/src/main/java/com/artipie/front/Service.java b/src/main/java/com/artipie/front/Service.java index e35013b..f4663b0 100644 --- a/src/main/java/com/artipie/front/Service.java +++ b/src/main/java/com/artipie/front/Service.java @@ -4,34 +4,24 @@ */ package com.artipie.front; -import com.amihaiemil.eoyaml.Yaml; -import com.artipie.front.api.ApiAuthFilter; -import com.artipie.front.api.NotFoundException; -import com.artipie.front.api.PostToken; -import com.artipie.front.api.Repositories; -import com.artipie.front.api.RepositoryPermissions; -import com.artipie.front.api.Storages; -import com.artipie.front.api.Users; -import com.artipie.front.auth.AccessFilter; -import com.artipie.front.auth.ApiTokens; -import com.artipie.front.auth.AuthByPassword; -import com.artipie.front.auth.Credentials; import com.artipie.front.internal.HealthRoute; -import com.artipie.front.misc.RequestPath; -import com.artipie.front.settings.ArtipieYaml; -import com.artipie.front.settings.RepoData; -import com.artipie.front.settings.RepoSettings; -import com.artipie.front.settings.YamlRepoPermissions; +import com.artipie.front.rest.AuthService; +import com.artipie.front.rest.RepositoryService; import com.artipie.front.ui.HbTemplateEngine; import com.artipie.front.ui.PostSignIn; -import com.artipie.front.ui.RepoPage; import com.artipie.front.ui.SignInPage; -import com.artipie.front.ui.UserPage; +import com.artipie.front.ui.repository.RepoAddConfig; +import com.artipie.front.ui.repository.RepoAddInfo; +import com.artipie.front.ui.repository.RepoEdit; +import com.artipie.front.ui.repository.RepoList; +import com.artipie.front.ui.repository.RepoRemove; +import com.artipie.front.ui.repository.RepoSave; +import com.artipie.front.ui.repository.RepositoryInfo; +import com.artipie.front.ui.repository.RepositoryTemplate; import com.fasterxml.jackson.core.JsonParseException; import com.jcabi.log.Logger; -import java.io.File; -import java.io.IOException; -import java.util.Random; +import java.util.Map; +import java.util.Optional; import javax.json.Json; import javax.json.JsonException; import org.apache.commons.cli.CommandLine; @@ -41,10 +31,11 @@ import org.apache.commons.cli.Option; import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; -import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.MimeTypes; import spark.ExceptionHandler; +import spark.ModelAndView; /** * Front service. @@ -57,7 +48,6 @@ */ @SuppressWarnings({"PMD.AvoidDuplicateLiterals", "PMD.ExcessiveMethodLength"}) public final class Service { - /** * Name for argument port for service. */ @@ -66,35 +56,34 @@ public final class Service { ); /** - * Name for argument config file for service. + * Name for argument artipie rest endpoint. */ - private static final Option CONFIG = Option.builder().option("c").longOpt("config") - .hasArg(true).desc("The path to artipie configuration file").required(true).build(); + private static final Option REST = new Option( + "r", "rest", true, "The artipie rest endpoint. Default value http://localhost:8086" + ); /** - * Api tokens. + * Name for argument artipie layout. */ - private final ApiTokens tkn; + private static final Option LAYOUT = new Option( + "l", "layout", true, "The artipie layout. Default value is flat" + ); /** - * Artipie configuration. + * Spark service instance. */ - @SuppressWarnings({"PMD.SingularField", "PMD.UnusedPrivateField"}) - private final ArtipieYaml settings; + private volatile spark.Service ignite; /** - * Spark service instance. + * Template engine. */ - private volatile spark.Service ignite; + private final HbTemplateEngine engine; /** * Service constructor. - * @param tkn Api tokens - * @param settings Artipie configuration */ - Service(final ApiTokens tkn, final ArtipieYaml settings) { - this.tkn = tkn; - this.settings = settings; + Service() { + this.engine = new HbTemplateEngine("/html"); } /** @@ -106,211 +95,133 @@ public final class Service { public static void main(final String... args) throws ParseException { final Options options = new Options(); options.addOption(Service.PORT); - options.addOption(Service.CONFIG); + options.addOption(Service.REST); + options.addOption(Service.LAYOUT); final CommandLineParser parser = new DefaultParser(); final CommandLine cmd; try { cmd = parser.parse(options, args); - final var service = new Service( - new ApiTokens(DigestUtils.sha1(System.getenv("TKN_KEY")), new Random()), - new ArtipieYaml( - Yaml.createYamlInput(new File(cmd.getOptionValue(Service.CONFIG))) - .readYamlMapping() - ) + final var service = new Service(); + service.start( + Integer.parseInt(new Param(Service.PORT, "ARTIPIE_PORT", "8080").get(cmd)), + new Param(Service.REST, "ARTIPIE_REST", "http://localhost:8086").get(cmd), + Optional.ofNullable(new Param(Service.LAYOUT, "ARTIPIE_LAYOUT", "flat").get(cmd)) + .map( + value -> { + final Layout result; + if (Layout.FLAT.toString().equals(value)) { + result = Layout.FLAT; + } else { + result = Layout.ORG; + } + return result; + }).orElse(Layout.FLAT) ); - service.start(Integer.parseInt(cmd.getOptionValue(Service.PORT, "8080"))); Runtime.getRuntime().addShutdownHook(new Thread(service::stop, "shutdown")); } catch (final ParseException ex) { final HelpFormatter formatter = new HelpFormatter(); formatter.printHelp("com.artipie.front.Service", options); throw ex; - } catch (final IOException ex) { - Logger.error(Service.class, "Failed to read artipie setting yaml"); - System.exit(1); } } /** * Start service. * @param port Port for service + * @param rest Artipie rest endpoint + * @param layout Artipie layout */ - void start(final int port) { + void start(final int port, final String rest, final Layout layout) { if (this.ignite != null) { throw new IllegalStateException("already started"); } Logger.info(this, "starting service on port: %d", port); this.ignite = spark.Service.ignite().port(port); this.ignite.get("/.health", new HealthRoute()); - final Credentials creds = this.settings.credentials(); - this.ignite.post( - "/token", - new PostToken(AuthByPassword.withCredentials(creds), this.tkn) - ); - this.ignite.path( - "/api", () -> { - this.ignite.before( - "/*", new ApiAuthFilter(new ApiAuthFilter.ApiTokenValidator(this.tkn)) - ); - this.ignite.before( - "/*", - new AccessFilter( - this.settings.accessPermissions(), this.settings.userPermissions() - ) - ); - this.ignite.path( - "/repositories", () -> { - final RepoSettings stn = new RepoSettings( - this.settings.layout(), this.settings.repoConfigsStorage() - ); - this.ignite.get( - "", MimeTypes.Type.APPLICATION_JSON.asString(), - new Repositories.GetAll(stn) - ); - final RequestPath path = new RequestPath().with(Repositories.REPO_PARAM); - this.ignite.get( - path.toString(), MimeTypes.Type.APPLICATION_JSON.asString(), - new Repositories.Get(stn) - ); - this.ignite.head(path.toString(), new Repositories.Head(stn)); - this.ignite.delete( - path.toString(), - new Repositories.Delete(stn, new RepoData(stn)) - ); - this.ignite.put(path.toString(), new Repositories.Put(stn)); - this.ignite.put( - path.with("move").toString(), - new Repositories.Move(stn, new RepoData(stn)) - ); - final RequestPath repo = this.repoPath(); - this.ignite.get( - repo.with("permissions").toString(), - new RepositoryPermissions.Get( - new YamlRepoPermissions(this.settings.repoConfigsStorage()) - ) - ); - this.ignite.put( - repo.with("permissions").with(RepositoryPermissions.NAME).toString(), - new RepositoryPermissions.Put( - new YamlRepoPermissions(this.settings.repoConfigsStorage()) - ) - ); - this.ignite.delete( - repo.with("permissions").with(RepositoryPermissions.NAME).toString(), - new RepositoryPermissions.Delete( - new YamlRepoPermissions(this.settings.repoConfigsStorage()) - ) - ); - this.ignite.patch( - repo.with("permissions").toString(), - new RepositoryPermissions.Patch( - new YamlRepoPermissions(this.settings.repoConfigsStorage()) - ) - ); - this.ignite.get( - repo.with("storages").toString(), - MimeTypes.Type.APPLICATION_JSON.asString(), - new Storages.GetAll(this.settings.repoConfigsStorage()) - ); - this.ignite.get( - repo.with("storages").with(Storages.ST_ALIAS).toString(), - new Storages.Get(this.settings.repoConfigsStorage()) - ); - this.ignite.head( - repo.with("storages").with(Storages.ST_ALIAS).toString(), - new Storages.Head(this.settings.repoConfigsStorage()) - ); - this.ignite.delete( - repo.with("storages").with(Storages.ST_ALIAS).toString(), - new Storages.Delete(this.settings.repoConfigsStorage()) - ); - this.ignite.put( - repo.with("storages").with(Storages.ST_ALIAS).toString(), - new Storages.Put(this.settings.repoConfigsStorage()) - ); - } - ); - this.ignite.path( - "/storages", () -> { - final RequestPath usr = this.userPath(); - this.ignite.get( - usr.toString(), MimeTypes.Type.APPLICATION_JSON.asString(), - new Storages.GetAll(this.settings.repoConfigsStorage()) - ); - this.ignite.get( - usr.with(Storages.ST_ALIAS).toString(), - MimeTypes.Type.APPLICATION_JSON.asString(), - new Storages.Get(this.settings.repoConfigsStorage()) - ); - this.ignite.head( - usr.with(Storages.ST_ALIAS).toString(), - new Storages.Head(this.settings.repoConfigsStorage()) - ); - this.ignite.delete( - usr.with(Storages.ST_ALIAS).toString(), - new Storages.Delete(this.settings.repoConfigsStorage()) - ); - this.ignite.put( - usr.with(Storages.ST_ALIAS).toString(), - new Storages.Put(this.settings.repoConfigsStorage()) - ); - } - ); - this.ignite.path( - "/users", () -> { - this.ignite.get( - "", MimeTypes.Type.APPLICATION_JSON.asString(), - new Users.GetAll(this.settings.users()) - ); - final String path = new RequestPath().with(Users.USER_PARAM).toString(); - this.ignite.get(path, new Users.GetUser(this.settings.users())); - this.ignite.put(path, new Users.Put(this.settings.users(), creds)); - this.ignite.head(path, new Users.Head(this.settings.users())); - this.ignite.delete(path, new Users.Delete(this.settings.users(), creds)); - } - ); - } - ); - final var engine = new HbTemplateEngine("/html"); this.ignite.path( "/signin", () -> { this.ignite.get( - "", MimeTypes.Type.APPLICATION_JSON.asString(), - new SignInPage(), engine + "", + MimeTypes.Type.APPLICATION_JSON.asString(), + new SignInPage(), + this.engine + ); + this.ignite.post( + "", + new PostSignIn(new AuthService(rest)) ); - this.ignite.post("", new PostSignIn(AuthByPassword.withCredentials(creds))); } ); this.ignite.path( "/dashboard", () -> { - final RepoSettings stn = new RepoSettings( - this.settings.layout(), this.settings.repoConfigsStorage() - ); - this.ignite.get("", new UserPage(stn), engine); - this.ignite.get( - new RequestPath().with(Users.USER_PARAM).toString(), new UserPage(stn), engine - ); this.ignite.get( - new RequestPath().with(Users.USER_PARAM) - .with(Repositories.REPO_PARAM).toString(), - new RepoPage.TemplateView(stn), engine - ); - this.ignite.post( - new RequestPath().with("api").with("repos").with(Users.USER_PARAM).toString(), - new RepoPage.Post(stn) + "", + (req, res) -> { + res.redirect("/dashboard/repository/list"); + return "Ok"; + } ); - this.ignite.get( - new RequestPath().with("api").with("repos").with(Users.USER_PARAM).toString(), - new RepoPage.Get() + final RepositoryService repository = new RepositoryService(rest); + final RepositoryInfo info = new RepositoryInfo(); + final RepositoryTemplate template = new RepositoryTemplate(); + this.ignite.path( + "/repository", () -> { + this.ignite.get( + "/list", + new RepoList(repository, layout), + this.engine + ); + if (layout == Layout.FLAT) { + this.ignite.get( + "/edit/:repo", + new RepoEdit(repository, layout, info), + this.engine + ); + this.ignite.post( + "/update/:repo", + new RepoSave(repository, layout), + this.engine + ); + this.ignite.post( + "/remove/:repo", + new RepoRemove(repository, layout), + this.engine + ); + } else { + this.ignite.get( + "/edit/:user/:repo", + new RepoEdit(repository, layout, info), + this.engine + ); + this.ignite.post( + "/update/:user/:repo", + new RepoSave(repository, layout), + this.engine + ); + this.ignite.post( + "/remove/:user/:repo", + new RepoRemove(repository, layout), + this.engine + ); + } + this.ignite.get("/add/info", new RepoAddInfo(), this.engine); + this.ignite.get( + "/add/config", + new RepoAddConfig(layout, info, template), + this.engine + ); + } ); } ); this.ignite.before(AuthFilters.AUTHENTICATE); this.ignite.before(AuthFilters.SESSION_ATTRS); - this.ignite.exception(NotFoundException.class, Service.error(HttpStatus.NOT_FOUND_404)); this.ignite.exception(JsonException.class, Service.error(HttpStatus.BAD_REQUEST_400)); this.ignite.exception(JsonParseException.class, Service.error(HttpStatus.BAD_REQUEST_400)); + this.ignite.exception(RestException.class, this.restError()); + this.ignite.exception(Exception.class, this.error()); this.ignite.awaitInitialization(); Logger.info(this, "service started on port: %d", this.ignite.port()); } @@ -325,32 +236,6 @@ void stop() { Logger.info(this, "service stopped"); } - /** - * Returns repository name path parameter. When artipie layout is org, repository name - * has username path prefix: uname/reponame. - * @return Repository name path parameter - */ - private RequestPath repoPath() { - RequestPath res = new RequestPath().with(Repositories.REPO_PARAM); - if ("org".equals(this.settings.layout())) { - res = new RequestPath().with(Users.USER_PARAM).with(Repositories.REPO_PARAM); - } - return res; - } - - /** - * Returns username path parameter. When artipie layout is org, username - * is required, otherwise - not. - * @return Username path parameter - */ - private RequestPath userPath() { - RequestPath res = new RequestPath(); - if ("org".equals(this.settings.layout())) { - res = res.with(Users.USER_PARAM); - } - return res; - } - /** * Handle exceptions by writing error in json body and returning * provided status. @@ -367,4 +252,96 @@ private static ExceptionHandler error(final int status) { rsp.status(status); }; } + + /** + * Handle RestException by rendering html-page with errorMessage and http status code + * received from rest service. + * @return Instance of {@link ExceptionHandler} + */ + private ExceptionHandler restError() { + return (exc, rqs, rsp) -> { + rsp.type(MimeTypes.Type.TEXT_HTML.asString()); + rsp.body( + this.engine.render( + new ModelAndView( + Map.of( + "errorMessage", exc.getMessage(), + "statusCode", exc.statusCode() + ), + "restError" + ) + ) + ); + }; + } + + /** + * Handle Exception by rendering html-page with errorMessage. + * @return Instance of {@link ExceptionHandler} + */ + private ExceptionHandler error() { + return (exc, rqs, rsp) -> { + rsp.type(MimeTypes.Type.TEXT_HTML.asString()); + rsp.body( + this.engine.render( + new ModelAndView( + Map.of( + "errorMessage", ExceptionUtils.getMessage(exc), + "stackTrace", ExceptionUtils.getStackTrace(exc) + ), + "error" + ) + ) + ); + }; + } + + /** + * Parameter. + * @since 1.0 + */ + private static class Param { + /** + * Option cmd argument. + */ + private final Option option; + + /** + * Environment argument. + */ + private final String env; + + /** + * Default value. + */ + private final String def; + + /** + * Ctor. + * @param option Option cmd argument name. + * @param envparam Environment argument name. + * @param def Default value. + */ + Param(final Option option, final String envparam, final String def) { + this.option = option; + this.env = envparam; + this.def = def; + } + + /** + * Get parameter from cmd-arguments or environment or by default. + * @param cmd Command line. + * @return Parameter value. + */ + public String get(final CommandLine cmd) { + String param = cmd.getOptionValue(this.option); + if (param == null) { + param = System.getenv(this.env); + if (param == null) { + param = this.def; + } + } + return param; + } + } } diff --git a/src/main/java/com/artipie/front/api/ApiAuthFilter.java b/src/main/java/com/artipie/front/api/ApiAuthFilter.java deleted file mode 100644 index fb78aea..0000000 --- a/src/main/java/com/artipie/front/api/ApiAuthFilter.java +++ /dev/null @@ -1,119 +0,0 @@ -/* - * The MIT License (MIT) Copyright (c) 2022 artipie.com - * https://github.com/artipie/front/LICENSE.txt - */ -package com.artipie.front.api; - -import com.artipie.front.RequestAttr; -import com.artipie.front.auth.ApiTokens; -import java.time.Instant; -import java.util.Optional; -import javax.json.Json; -import org.eclipse.jetty.http.HttpStatus; -import spark.Filter; -import spark.Request; -import spark.Response; -import spark.Spark; - -/** - * Spark filter for API authentication. - * @since 1.0 - */ -public final class ApiAuthFilter implements Filter { - - /** - * Auth token validator. - */ - private final TokenValidator validator; - - /** - * New API auth filter. - * @param validator Token validator - */ - public ApiAuthFilter(final TokenValidator validator) { - this.validator = validator; - } - - @Override - public void handle(final Request request, final Response response) throws Exception { - try { - final var uid = this.validator.validate( - Optional.ofNullable(request.headers("Authorization")).orElse(""), - Instant.now() - ); - RequestAttr.Standard.USER_ID.write(request, uid); - } catch (final ValidationException vex) { - Spark.halt( - HttpStatus.UNAUTHORIZED_401, - Json.createObjectBuilder() - .add("error", vex.getMessage()) - .build().toString() - ); - } - } - - /** - * Token validator API. - * @since 1.0 - */ - @FunctionalInterface - public interface TokenValidator { - - /** - * Validate auth token. - * @param token Token data - * @param time Current time - * @return User ID - * @throws ValidationException If token is not valid - */ - String validate(String token, Instant time) throws ValidationException; - } - - /** - * Api token validator implementation. - * @since 0.1 - */ - public static final class ApiTokenValidator implements TokenValidator { - - /** - * Api tokens. - */ - private final ApiTokens tkn; - - /** - * Ctor. - * @param tkn Api tokens - */ - public ApiTokenValidator(final ApiTokens tkn) { - this.tkn = tkn; - } - - @Override - public String validate(final String token, final Instant time) throws ValidationException { - if (this.tkn.validate(token)) { - final ApiTokens.Token valid = ApiTokens.Token.parse(token); - if (!valid.expired(time)) { - return valid.user(); - } - } - throw new ValidationException("Invalid token"); - } - } - - /** - * Token validation exception. - * @since 1.0 - */ - public static final class ValidationException extends Exception { - - public static final long serialVersionUID = 0L; - - /** - * New exception. - * @param details Validation details - */ - public ValidationException(final String details) { - super(details); - } - } -} diff --git a/src/main/java/com/artipie/front/api/NotFoundException.java b/src/main/java/com/artipie/front/api/NotFoundException.java deleted file mode 100644 index c280ec2..0000000 --- a/src/main/java/com/artipie/front/api/NotFoundException.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * The MIT License (MIT) Copyright (c) 2022 artipie.com - * https://github.com/artipie/front/LICENSE.txt - */ -package com.artipie.front.api; - -/** - * Not found error, to be thrown when any requested item is not found. - * This exception will be handled as HTTP 404 status. - * @since 0.1 - */ -public class NotFoundException extends RuntimeException { - - private static final long serialVersionUID = 1L; - - /** - * Ctor. - * @param msg Error message - * @param err Cause - */ - public NotFoundException(final String msg, final Throwable err) { - super(msg, err); - } - - /** - * Ctor. - * @param msg Error message - */ - public NotFoundException(final String msg) { - super(msg); - } - -} diff --git a/src/main/java/com/artipie/front/api/PostToken.java b/src/main/java/com/artipie/front/api/PostToken.java deleted file mode 100644 index d42043f..0000000 --- a/src/main/java/com/artipie/front/api/PostToken.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * The MIT License (MIT) Copyright (c) 2022 artipie.com - * https://github.com/artipie/front/LICENSE.txt - */ -package com.artipie.front.api; - -import com.artipie.front.auth.ApiTokens; -import com.artipie.front.auth.AuthByPassword; -import java.io.StringReader; -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.Optional; -import javax.json.Json; -import javax.json.JsonObject; -import org.eclipse.jetty.http.HttpStatus; -import org.eclipse.jetty.http.MimeTypes; -import spark.Request; -import spark.Response; -import spark.Route; - -/** - * Endpoint to generate token for user. Accepts POST request with json body - * with fields `name` and `pass` and returns json with `token` field. - * @since 0.1 - */ -public final class PostToken implements Route { - - /** - * Password-based authentication. - */ - private final AuthByPassword auth; - - /** - * Tokens. - */ - private final ApiTokens tkn; - - /** - * Ctor. - * @param auth Password-based authentication - * @param tkn Tokens - */ - public PostToken(final AuthByPassword auth, final ApiTokens tkn) { - this.auth = auth; - this.tkn = tkn; - } - - @Override - public String handle(final Request request, final Response response) { - final JsonObject json = Json.createReader(new StringReader(request.body())).readObject(); - final Optional usr = - this.auth.authenticate(json.getString("name"), json.getString("pass")); - final JsonObject res; - response.type(MimeTypes.Type.APPLICATION_JSON.toString()); - if (usr.isEmpty()) { - response.status(HttpStatus.UNAUTHORIZED_401); - res = Json.createObjectBuilder().add("err", "Invalid credentials").build(); - } else { - response.status(HttpStatus.CREATED_201); - res = Json.createObjectBuilder().add( - // @checkstyle MagicNumberCheck (1 line) - "token", this.tkn.token(usr.get(), Instant.now().plus(30, ChronoUnit.DAYS)) - ).build(); - } - return res.toString(); - } -} diff --git a/src/main/java/com/artipie/front/api/Repositories.java b/src/main/java/com/artipie/front/api/Repositories.java deleted file mode 100644 index b9a3fcf..0000000 --- a/src/main/java/com/artipie/front/api/Repositories.java +++ /dev/null @@ -1,320 +0,0 @@ -/* - * The MIT License (MIT) Copyright (c) 2022 artipie.com - * https://github.com/artipie/front/LICENSE.txt - */ -package com.artipie.front.api; - -import com.artipie.front.RequestAttr; -import com.artipie.front.misc.Json2Yaml; -import com.artipie.front.misc.RequestPath; -import com.artipie.front.misc.Yaml2Json; -import com.artipie.front.settings.RepoData; -import com.artipie.front.settings.RepoSettings; -import java.io.StringReader; -import java.nio.charset.StandardCharsets; -import java.util.Map; -import java.util.Optional; -import javax.json.Json; -import javax.json.JsonArrayBuilder; -import javax.json.JsonObject; -import javax.json.JsonObjectBuilder; -import javax.json.JsonValue; -import org.eclipse.jetty.http.HttpStatus; -import org.eclipse.jetty.http.MimeTypes; -import spark.Request; -import spark.Response; -import spark.Route; - -/** - * Repositories API endpoints. - * @since 0.1 - */ -@SuppressWarnings("PMD.AvoidDuplicateLiterals") -public final class Repositories { - - /** - * Repository name request parameter. - */ - public static final RequestPath.Param REPO_PARAM = new RequestPath.Param("repo"); - - /** - * Repository settings yaml section `repo` name. - */ - private static final String REPO = "repo"; - - /** - * Ctor. - */ - private Repositories() { - } - - /** - * Handle `GET` request to obtain repositories list, request line example: - * GET /repositories - * Returns repositories list. - * @since 0.1 - */ - public static final class GetAll implements Route { - - /** - * Artipie repositories settings. - */ - private final RepoSettings stngs; - - /** - * Ctor. - * - * @param storage Artipie repositories settings storage - */ - public GetAll(final RepoSettings storage) { - this.stngs = storage; - } - - @Override - public String handle(final Request request, final Response response) { - JsonArrayBuilder json = Json.createArrayBuilder(); - for (final String name : this.stngs.list(Optional.empty())) { - json = json.add(Json.createObjectBuilder().add("fullName", name).build()); - } - response.type(MimeTypes.Type.APPLICATION_JSON.toString()); - return json.build().toString(); - } - } - - /** - * Handle `GET` request to obtain repository details, request line example: - * GET /repositories/{repo_name} - * where {repo_name} is the name of the repository. In the case of `org` layout, - * repository owner is obtained from request attributes. - * - * @since 0.1 - */ - public static final class Get implements Route { - - /** - * Repository settings. - */ - private final RepoSettings stn; - - /** - * Ctor. - * @param stn Repository settings - */ - public Get(final RepoSettings stn) { - this.stn = stn; - } - - @Override - public String handle(final Request request, final Response response) { - final JsonObject repo = new Yaml2Json().apply( - new String( - this.stn.value( - REPO_PARAM.parse(request), - RequestAttr.Standard.USER_ID.readOrThrow(request) - ), - StandardCharsets.UTF_8 - ) - ).asJsonObject().getJsonObject(Repositories.REPO); - JsonObjectBuilder builder = Json.createObjectBuilder(); - for (final Map.Entry item : repo.entrySet()) { - if (!"permissions".equals(item.getKey())) { - builder = builder.add(item.getKey(), item.getValue()); - } - } - response.type(MimeTypes.Type.APPLICATION_JSON.toString()); - return Json.createObjectBuilder().add(Repositories.REPO, builder.build()) - .build().toString(); - } - } - - /** - * Handle `DELETE` request to delete repository, request line example: - * DELETE /repositories/{repo_name} - * where {repo_name} is the name of the repository. In the case of `org` layout, - * repository owner is obtained from request attributes. - * - * @since 0.1 - */ - public static final class Delete implements Route { - - /** - * Repository settings. - */ - private final RepoSettings stn; - - /** - * Repository data. - */ - private final RepoData data; - - /** - * Ctor. - * @param stn Repository settings - * @param data Repository data - */ - public Delete(final RepoSettings stn, final RepoData data) { - this.stn = stn; - this.data = data; - } - - @Override - public Object handle(final Request request, final Response response) { - this.data.remove( - REPO_PARAM.parse(request), - RequestAttr.Standard.USER_ID.readOrThrow(request) - ).thenRun( - () -> this.stn.delete( - REPO_PARAM.parse(request), - RequestAttr.Standard.USER_ID.readOrThrow(request) - ) - ); - return ""; - } - } - - /** - * Handle `HEAD` request to check if repository exists, request line example: - * HEAD /repositories/{repo_name} - * where {repo_name} is the name of the repository. In the case of `org` layout, - * repository owner is obtained from request attributes. - * - * @since 0.1 - */ - public static final class Head implements Route { - - /** - * Repositories settings. - */ - private final RepoSettings stn; - - /** - * Ctor. - * @param stn Repositories settings - */ - public Head(final RepoSettings stn) { - this.stn = stn; - } - - @Override - public Object handle(final Request request, final Response response) { - this.stn.key( - REPO_PARAM.parse(request), - RequestAttr.Standard.USER_ID.readOrThrow(request) - ); - return ""; - } - } - - /** - * Handle `PUT` request to add the repository, request line example: - * PUT /repositories/{repo_name} - * where {repo_name} is the name of the repository. In the case of `org` layout, - * repository owner is obtained from request attributes. Json with repository - * seettings is expected in the request body. - * - * @since 0.1 - * @checkstyle ReturnCountCheck (500 lines) - */ - public static final class Put implements Route { - - /** - * Repository settings. - */ - private final RepoSettings stn; - - /** - * Ctor. - * @param stn Repository settings - */ - public Put(final RepoSettings stn) { - this.stn = stn; - } - - @Override - @SuppressWarnings("PMD.OnlyOneReturn") - public Object handle(final Request request, final Response response) { - final String param = REPO_PARAM.parse(request); - final String uid = RequestAttr.Standard.USER_ID.readOrThrow(request); - if (this.stn.exists(param, uid)) { - response.status(HttpStatus.CONFLICT_409); - return String.format("Repository %s already exists", param); - } - final JsonObject body = Json.createReader(new StringReader(request.body())) - .readObject(); - final JsonObject repo = body.getJsonObject(Repositories.REPO); - if (repo == null) { - response.status(HttpStatus.BAD_REQUEST_400); - return "Section `repo` is required"; - } - if (!repo.containsKey("type")) { - response.status(HttpStatus.BAD_REQUEST_400); - return "Repository type is required"; - } - if (!repo.containsKey("storage")) { - response.status(HttpStatus.BAD_REQUEST_400); - return "Repository storage is required"; - } - this.stn.save( - param, uid, - new Json2Yaml().apply(body.toString()).toString().getBytes(StandardCharsets.UTF_8) - ); - response.status(HttpStatus.CREATED_201); - return ""; - } - } - - /** - * Handle `PUT` request to rename the repository, request line example: - * PUT /repositories/{repo_name}/move - * where {repo_name} is the name of the repository. In the case of `org` layout, - * repository owner is obtained from request attributes. Json with repository - * new name (field `new_name`) is expected in the request body. - * - * @since 0.1 - * @checkstyle ReturnCountCheck (500 lines) - */ - public static final class Move implements Route { - - /** - * Repository settings. - */ - private final RepoSettings stn; - - /** - * Repository data. - */ - private final RepoData data; - - /** - * Ctor. - * @param stn Repository settings - * @param data Repository data - */ - public Move(final RepoSettings stn, final RepoData data) { - this.stn = stn; - this.data = data; - } - - @Override - @SuppressWarnings("PMD.OnlyOneReturn") - public Object handle(final Request request, final Response response) { - final String param = REPO_PARAM.parse(request); - final String uid = RequestAttr.Standard.USER_ID.readOrThrow(request); - if (!this.stn.exists(param, uid)) { - response.status(HttpStatus.BAD_REQUEST_400); - return String.format("Repository does not %s exist", param); - } - final String nname = Json.createReader(new StringReader(request.body())) - .readObject().getString("new_name"); - if (nname == null) { - response.status(HttpStatus.BAD_REQUEST_400); - return "Field `new_name` is required"; - } - this.data.move(param, uid, nname).thenRun( - () -> this.stn.move(param, uid, nname) - ); - response.status(HttpStatus.OK_200); - return ""; - } - } -} diff --git a/src/main/java/com/artipie/front/api/RepositoryPermissions.java b/src/main/java/com/artipie/front/api/RepositoryPermissions.java deleted file mode 100644 index 5a91944..0000000 --- a/src/main/java/com/artipie/front/api/RepositoryPermissions.java +++ /dev/null @@ -1,184 +0,0 @@ -/* - * The MIT License (MIT) Copyright (c) 2022 artipie.com - * https://github.com/artipie/front/LICENSE.txt - */ -package com.artipie.front.api; - -import com.artipie.front.misc.RequestPath; -import com.artipie.front.settings.RepoPermissions; -import java.io.StringReader; -import java.util.Optional; -import javax.json.Json; -import javax.json.JsonObject; -import org.eclipse.jetty.http.HttpStatus; -import org.eclipse.jetty.http.MimeTypes; -import spark.Request; -import spark.Response; -import spark.Route; - -/** - * Repository permissions endpoint. - * @since 0.1 - */ -public final class RepositoryPermissions { - - /** - * Name of the user to update permissions for. - */ - public static final RequestPath.Param NAME = new RequestPath.Param("uname"); - - /** - * Private ctor. - */ - private RepositoryPermissions() { - } - - /** - * Repository name from request. - * @param request Spark request - * @return Repository name - */ - private static String repoNameFromRq(final Request request) { - return Optional.ofNullable(Users.USER_PARAM.parse(request)).map(usr -> usr.concat("/")) - .orElse("").concat(Repositories.REPO_PARAM.parse(request)); - } - - /** - * Handle `PUT` request to add repository permissions, request line format: - * PUT /repo/{owner_name}/{repo_name}/permissions/{uname} - * where {owner_name} is required for `org` layout only, {uname} is the name of - * the user to add permission for. - * @since 0.1 - */ - public static final class Put implements Route { - - /** - * Repository permissions. - * @since 0.1 - */ - private final RepoPermissions perms; - - /** - * Ctor. - * @param perms Repository permissions - */ - public Put(final RepoPermissions perms) { - this.perms = perms; - } - - @Override - public Object handle(final Request request, final Response response) { - this.perms.add( - RepositoryPermissions.repoNameFromRq(request), - RepositoryPermissions.NAME.parse(request), - Json.createReader(new StringReader(request.body())).readArray() - ); - response.status(HttpStatus.CREATED_201); - return ""; - } - } - - /** - * Handle `GET` request to add repository permissions, request line format: - * PUT /repo/{owner_name}/{repo_name}/permissions - * where {owner_name} is required for `org` layout only. - * @since 0.1 - */ - public static final class Get implements Route { - - /** - * Repository settings section `permissions` name. - */ - private static final String PERMISSIONS = "permissions"; - - /** - * Repository settings. - */ - private final RepoPermissions stn; - - /** - * Ctor. - * @param stn Repository settings - */ - public Get(final RepoPermissions stn) { - this.stn = stn; - } - - @Override - public String handle(final Request request, final Response response) { - final JsonObject res = this.stn.get(RepositoryPermissions.repoNameFromRq(request)); - response.type(MimeTypes.Type.APPLICATION_JSON.toString()); - return Json.createObjectBuilder().add(RepositoryPermissions.Get.PERMISSIONS, res) - .build().toString(); - } - } - - /** - * Handle `DELETE` request to remove repository permissions, request line format: - * DELETE /repo/{owner_name}/{repo_name}/permissions/{uname} - * where {owner_name} is required for `org` layout only, {uname} is the name of - * the user to add permission for. - * @since 0.1 - */ - public static final class Delete implements Route { - - /** - * Repository permissions. - * @since 0.1 - */ - private final RepoPermissions perms; - - /** - * Ctor. - * @param perms Repository permissions - */ - public Delete(final RepoPermissions perms) { - this.perms = perms; - } - - @Override - public Object handle(final Request request, final Response response) { - this.perms.delete( - RepositoryPermissions.repoNameFromRq(request), - RepositoryPermissions.NAME.parse(request) - ); - response.status(HttpStatus.OK_200); - return ""; - } - } - - /** - * Handle `PATCH` request to update repository permissions, request line format: - * PATCH /repo/{owner_name}/{repo_name}/permissions - * where {owner_name} is required for `org` layout only, json object with permissions - * to update is expected in the request body. - * @since 0.1 - */ - public static final class Patch implements Route { - - /** - * Repository permissions. - * - * @since 0.1 - */ - private final RepoPermissions perms; - - /** - * Ctor. - * @param perms Repository permissions - */ - public Patch(final RepoPermissions perms) { - this.perms = perms; - } - - @Override - public Object handle(final Request request, final Response response) { - this.perms.patch( - RepositoryPermissions.repoNameFromRq(request), - Json.createReader(new StringReader(request.body())).readObject() - ); - response.status(HttpStatus.OK_200); - return ""; - } - } -} diff --git a/src/main/java/com/artipie/front/api/Storages.java b/src/main/java/com/artipie/front/api/Storages.java deleted file mode 100644 index 95257b1..0000000 --- a/src/main/java/com/artipie/front/api/Storages.java +++ /dev/null @@ -1,240 +0,0 @@ -/* - * The MIT License (MIT) Copyright (c) 2022 artipie.com - * https://github.com/artipie/front/LICENSE.txt - */ -package com.artipie.front.api; - -import com.artipie.asto.Key; -import com.artipie.asto.blocking.BlockingStorage; -import com.artipie.front.misc.RequestPath; -import com.artipie.front.settings.YamlStorages; -import java.io.StringReader; -import java.util.Optional; -import javax.json.Json; -import javax.json.JsonObjectBuilder; -import org.eclipse.jetty.http.HttpStatus; -import org.eclipse.jetty.http.MimeTypes; -import spark.Request; -import spark.Response; -import spark.Route; - -/** - * Storages API endpoints. - * @since 0.1 - */ -public final class Storages { - - /** - * Storage alias path param. - */ - public static final RequestPath.Param ST_ALIAS = new RequestPath.Param("alias"); - - /** - * Ctor. - */ - private Storages() { - } - - /** - * Get repository key from request. - * @param request Spark request - * @return Repository key if present - */ - private static Optional repoFromRq(final Request request) { - final Optional usr = Optional.ofNullable(Users.USER_PARAM.parse(request)) - .map(Key.From::new); - Optional repo = Optional.ofNullable(Repositories.REPO_PARAM.parse(request)) - .map(Key.From::new); - if (usr.isPresent()) { - repo = repo.map(key -> new Key.From(usr.get(), key)).or(() -> usr); - } - return repo; - } - - /** - * Handle `GET` request to obtain storages list, request line example: - * GET /repositories/{owner_name}/{repo_name}/storages - * GET /storages/{owner_name} - * for common or repository storages, {owner_name} is required only for `org` layout. - * @since 0.1 - */ - public static final class GetAll implements Route { - - /** - * Artipie storage. - */ - private final BlockingStorage strgs; - - /** - * Ctor. - * - * @param strgs Artipie storages - */ - public GetAll(final BlockingStorage strgs) { - this.strgs = strgs; - } - - @Override - public String handle(final Request request, final Response response) { - final JsonObjectBuilder res = Json.createObjectBuilder(); - new YamlStorages(Storages.repoFromRq(request), this.strgs).list() - .forEach(item -> res.add(item.alias(), item.info())); - response.type(MimeTypes.Type.APPLICATION_JSON.asString()); - return Json.createObjectBuilder().add("storages", res.build()).build().toString(); - } - } - - /** - * Handle `GET` request to obtain storage alias details, request line example: - * GET /repositories/{owner_name}/{repo}/storages/{alias} - * GET /storages/{owner_name}/{alias} - * for repository and common storages, {owner_name} is required only for `org` layout, - * {alias} if the name of storage alias to get info about. - * @since 0.1 - * @checkstyle ReturnCountCheck (500 lines) - */ - public static final class Get implements Route { - - /** - * Artipie storage. - */ - private final BlockingStorage strgs; - - /** - * Ctor. - * @param strgs Artipie storage - */ - public Get(final BlockingStorage strgs) { - this.strgs = strgs; - } - - @Override - @SuppressWarnings("PMD.OnlyOneReturn") - public String handle(final Request request, final Response response) { - final Optional res = - new YamlStorages(Storages.repoFromRq(request), this.strgs).list().stream().filter( - item -> item.alias().equals(Storages.ST_ALIAS.parse(request)) - ).findFirst(); - if (res.isPresent()) { - return res.get().info().toString(); - } else { - response.status(HttpStatus.NOT_FOUND_404); - return null; - } - } - } - - /** - * Handle `HEAD` request to check is storage alias exists, request line example: - * HEAD /repositories/{owner_name}/{repo}/storages/{alias} - * HEAD /storages/{owner_name}/{alias} - * for repository and common storages, {owner_name} is required only for `org` layout, - * {alias} if the name of storage alias to get info about. - * @since 0.1 - */ - public static final class Head implements Route { - - /** - * Artipie storage. - */ - private final BlockingStorage strgs; - - /** - * Ctor. - * @param strgs Artipie storage - */ - public Head(final BlockingStorage strgs) { - this.strgs = strgs; - } - - @Override - public Object handle(final Request request, final Response response) { - final Optional res = - new YamlStorages(Storages.repoFromRq(request), this.strgs).list().stream().filter( - item -> item.alias().equals(Storages.ST_ALIAS.parse(request)) - ).findFirst(); - if (res.isPresent()) { - response.status(HttpStatus.FOUND_302); - } else { - response.status(HttpStatus.NOT_FOUND_404); - } - return ""; - } - } - - /** - * Handle `DELETE` request to delete storage alias, request line example: - * DELETE /repositories/{owner_name}/{repo}/storages/{alias} - * DELETE /storages/{owner_name}/{alias} - * for repository and common storages, {owner_name} is required only for `org` layout, - * {alias} if the name of storage alias to delete. - * @since 0.1 - */ - public static final class Delete implements Route { - - /** - * Artipie storage. - */ - private final BlockingStorage strgs; - - /** - * Ctor. - * - * @param strgs Artipie storages - */ - public Delete(final BlockingStorage strgs) { - this.strgs = strgs; - } - - @Override - public Object handle(final Request request, final Response response) { - new YamlStorages(Storages.repoFromRq(request), this.strgs) - .remove(Storages.ST_ALIAS.parse(request)); - response.status(HttpStatus.OK_200); - return ""; - } - } - - /** - * Handle `PUT` request to add storage alias, request line example: - * PUT /repositories/{owner_name}/{repo}/storages/{alias} - * PUT /storages/{owner_name}/{alias} - * for repository and common storages, {owner_name} is required only for `org` layout, - * {alias} if the name of storage alias to delete. - * Request body is expected to have new storage alias settings in json format. - * @since 0.1 - */ - public static final class Put implements Route { - - /** - * Artipie storage. - */ - private final BlockingStorage strgs; - - /** - * Ctor. - * - * @param strgs Artipie storages - */ - public Put(final BlockingStorage strgs) { - this.strgs = strgs; - } - - @Override - public Object handle(final Request request, final Response response) { - final YamlStorages storages = - new YamlStorages(Storages.repoFromRq(request), this.strgs); - final String alias = Storages.ST_ALIAS.parse(request); - if (storages.list().stream().anyMatch(item -> item.alias().equals(alias))) { - response.status(HttpStatus.CONFLICT_409); - } else { - storages.add( - alias, Json.createReader(new StringReader(request.body())).readObject() - ); - response.status(HttpStatus.CREATED_201); - } - return ""; - } - } - -} diff --git a/src/main/java/com/artipie/front/api/Users.java b/src/main/java/com/artipie/front/api/Users.java deleted file mode 100644 index 6005ff1..0000000 --- a/src/main/java/com/artipie/front/api/Users.java +++ /dev/null @@ -1,257 +0,0 @@ -/* - * The MIT License (MIT) Copyright (c) 2022 artipie.com - * https://github.com/artipie/front/LICENSE.txt - */ -package com.artipie.front.api; - -import com.artipie.front.auth.Credentials; -import com.artipie.front.auth.User; -import com.artipie.front.misc.RequestPath; -import java.io.StringReader; -import java.util.Optional; -import javax.json.Json; -import javax.json.JsonArrayBuilder; -import javax.json.JsonObject; -import javax.json.JsonObjectBuilder; -import org.eclipse.jetty.http.HttpStatus; -import spark.Request; -import spark.Response; -import spark.Route; - -/** - * Users API endpoints. - * @since 0.1 - */ -public final class Users { - - /** - * User request line parameter. - */ - public static final RequestPath.Param USER_PARAM = new RequestPath.Param("user"); - - /** - * Field `email`. - */ - private static final String EMAIL = "email"; - - /** - * Field `groups`. - */ - private static final String GROUPS = "groups"; - - /** - * Ctor. - */ - private Users() { - } - - /** - * Handle `GET` request to obtain users list, request line example: - * GET /users - * Returns json object with users list. - * @since 0.1 - */ - public static final class GetAll implements Route { - - /** - * Artipie users. - */ - private final com.artipie.front.auth.Users ausers; - - /** - * Ctor. - * - * @param creds Artipie users - */ - public GetAll(final com.artipie.front.auth.Users creds) { - this.ausers = creds; - } - - @Override - public String handle(final Request request, final Response response) { - final JsonObjectBuilder res = Json.createObjectBuilder(); - for (final User usr : this.ausers.list()) { - final JsonObjectBuilder builder = Json.createObjectBuilder(); - usr.email().ifPresent(email -> builder.add(Users.EMAIL, email)); - if (!usr.groups().isEmpty()) { - final JsonArrayBuilder arr = Json.createArrayBuilder(); - usr.groups().forEach(arr::add); - builder.add(Users.GROUPS, arr); - } - res.add(usr.uid(), builder.build()); - } - return res.build().toString(); - } - } - - /** - * Handle `GET` request to obtain user details, request line example: - * GET /users/{uid} - * where {uid} is the name of the user. - * @since 0.1 - * @checkstyle ReturnCountCheck (500 lines) - */ - public static final class GetUser implements Route { - - /** - * Artipie users. - */ - private final com.artipie.front.auth.Users users; - - /** - * Ctor. - * @param users Artipie users - */ - public GetUser(final com.artipie.front.auth.Users users) { - this.users = users; - } - - @Override - @SuppressWarnings("PMD.OnlyOneReturn") - public String handle(final Request request, final Response response) { - final String name = USER_PARAM.parse(request); - final Optional user = this.users.list().stream() - .filter(usr -> usr.uid().equals(name)).findFirst(); - if (user.isPresent()) { - final JsonObjectBuilder json = Json.createObjectBuilder(); - user.get().email().ifPresent(email -> json.add(Users.EMAIL, email)); - final JsonArrayBuilder arr = Json.createArrayBuilder(); - user.get().groups().forEach(arr::add); - json.add(Users.GROUPS, arr.build()); - return Json.createObjectBuilder().add(name, json.build()).build().toString(); - } else { - response.status(HttpStatus.NOT_FOUND_404); - return null; - } - } - } - - /** - * Handle `DELETE` request to delete the user, request line example: - * DELETE /users/{uid} - * where {uid} is the name of the user. - * @since 0.1 - */ - public static final class Delete implements Route { - - /** - * Artipie users. - */ - private final com.artipie.front.auth.Users users; - - /** - * Front service credentials. - */ - private final Credentials creds; - - /** - * Ctor. - * @param users Artipie users - * @param creds Front service credentials - */ - public Delete(final com.artipie.front.auth.Users users, final Credentials creds) { - this.users = users; - this.creds = creds; - } - - @Override - public Object handle(final Request request, final Response response) { - this.users.remove(USER_PARAM.parse(request)); - this.creds.reload(); - response.status(HttpStatus.OK_200); - return ""; - } - } - - /** - * Handle `HEAD` request to check if the user exists, request line example: - * HEAD /users/{uid} - * where {uid} is the name of the user. - * @since 0.1 - */ - public static final class Head implements Route { - - /** - * Artipie users. - */ - private final com.artipie.front.auth.Users users; - - /** - * Ctor. - * @param users Artipie users - */ - public Head(final com.artipie.front.auth.Users users) { - this.users = users; - } - - @Override - public Object handle(final Request request, final Response response) { - if (this.users.list().stream() - .noneMatch(usr -> usr.uid().equals(USER_PARAM.parse(request)))) { - response.status(HttpStatus.NOT_FOUND_404); - } else { - response.status(HttpStatus.OK_200); - } - return ""; - } - } - - /** - * Handle `PUT` request create user, request line example: - * PUT /users/{uid} - * where {uid} is the name of the user, json with user info is expected in the - * request body. - * @since 0.1 - * @checkstyle ReturnCountCheck (500 lines) - */ - public static final class Put implements Route { - - /** - * Artipie users. - */ - private final com.artipie.front.auth.Users users; - - /** - * Front service credentials. - */ - private final Credentials creds; - - /** - * Ctor. - * @param users Artipie users - * @param creds Front service credentials - */ - public Put(final com.artipie.front.auth.Users users, final Credentials creds) { - this.users = users; - this.creds = creds; - } - - @Override - @SuppressWarnings("PMD.OnlyOneReturn") - public Object handle(final Request request, final Response response) { - final String name = USER_PARAM.parse(request); - if (this.users.list().stream().anyMatch(usr -> name.equals(usr.uid()))) { - response.status(HttpStatus.CONFLICT_409); - return String.format("User %s already exists", name); - } - final JsonObject user = Json.createReader(new StringReader(request.body())).readObject() - .getJsonObject(name); - if (user == null) { - response.status(HttpStatus.BAD_REQUEST_400); - return "User info json is expected"; - } - if (!user.containsKey("type")) { - response.status(HttpStatus.BAD_REQUEST_400); - return "Password type field `type` is required"; - } - if (!user.containsKey("pass")) { - response.status(HttpStatus.BAD_REQUEST_400); - return "Password field `pass` is required"; - } - this.users.add(user, name); - this.creds.reload(); - response.status(HttpStatus.CREATED_201); - return ""; - } - } -} diff --git a/src/main/java/com/artipie/front/auth/AccessFilter.java b/src/main/java/com/artipie/front/auth/AccessFilter.java deleted file mode 100644 index a840ee2..0000000 --- a/src/main/java/com/artipie/front/auth/AccessFilter.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * The MIT License (MIT) Copyright (c) 2022 artipie.com - * https://github.com/artipie/front/LICENSE.txt - */ -package com.artipie.front.auth; - -import com.artipie.front.RequestAttr; -import org.eclipse.jetty.http.HttpStatus; -import spark.Filter; -import spark.Request; -import spark.Response; -import spark.Spark; - -/** - * Access HTTP filter. - * @since 1.0 - */ -@SuppressWarnings("PMD.AvoidDuplicateLiterals") -public final class AccessFilter implements Filter { - - /** - * Access permissions. - */ - private final AccessPermissions access; - - /** - * Permissions. - */ - private final UserPermissions perms; - - /** - * Access filter. - * @param access Access permissions - * @param perms Permissions - */ - public AccessFilter(final AccessPermissions access, final UserPermissions perms) { - this.access = access; - this.perms = perms; - } - - @Override - public void handle(final Request req, final Response rsp) throws Exception { - final var uid = RequestAttr.Standard.USER_ID.read(req); - if (uid.isEmpty()) { - Spark.halt(HttpStatus.UNAUTHORIZED_401, "Authentication required"); - } - final boolean allowed = this.access.get( - req.pathInfo().replace("/api", ""), req.requestMethod() - ).stream().anyMatch(perm -> this.perms.allowed(uid.get(), perm)); - if (!allowed) { - Spark.halt(HttpStatus.FORBIDDEN_403, "Request is not allowed"); - } - } -} diff --git a/src/main/java/com/artipie/front/auth/AccessPermissions.java b/src/main/java/com/artipie/front/auth/AccessPermissions.java deleted file mode 100644 index 75ab581..0000000 --- a/src/main/java/com/artipie/front/auth/AccessPermissions.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * The MIT License (MIT) Copyright (c) 2022 artipie.com - * https://github.com/artipie/front/LICENSE.txt - */ -package com.artipie.front.auth; - -import com.amihaiemil.eoyaml.YamlMapping; -import com.amihaiemil.eoyaml.YamlNode; -import io.vavr.collection.Stream; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.regex.Pattern; -import java.util.stream.Collectors; -import org.apache.commons.lang3.tuple.Pair; - -/** - * Permissions to access the endpoint. - * @since 0.1 - */ -public interface AccessPermissions { - - /** - * Fake access permissions for tests usages. - */ - AccessPermissions STUB = (line, method) -> Collections.emptyList(); - - /** - * Obtain the list of permissions, required to access the endpoint. - * @param line Request line - * @param method Request method - * @return Permissions list - */ - Collection get(String line, String method); - - /** - * Permissions to access the endpoint from yaml file. - * @since 0.1 - */ - final class FromYaml implements AccessPermissions { - - /** - * Access permissions map: the pair of patterns to match - * request line and request method and permissions list. - */ - private final Map, List> yaml; - - /** - * Ctor. - * @param yaml Access permissions yaml - */ - public FromYaml(final YamlMapping yaml) { - this.yaml = FromYaml.read(yaml); - } - - @Override - public Collection get(final String line, final String method) { - return Stream.concat( - this.yaml.entrySet().stream().filter( - entry -> { - final Pair pair = entry.getKey(); - return pair.getKey().matcher(line).matches() - && pair.getValue().matcher(method).matches(); - } - ).map(Map.Entry::getValue).collect(Collectors.toList()) - ).collect(Collectors.toList()); - } - - /** - * Reads yaml mapping into the map of the pair of patterns to match - * request line and request method and permissions list. - * @param yaml Yaml to read - * @return The access permissions map - */ - private static Map, List> read(final YamlMapping yaml) { - final Map, List> res = new HashMap<>(yaml.keys().size()); - for (final YamlNode line : yaml.keys()) { - final YamlMapping mapping = yaml.yamlMapping(line); - for (final YamlNode method : mapping.keys()) { - res.put( - Pair.of( - Pattern.compile(FromYaml.unquote(line.asScalar().value())), - Pattern.compile(FromYaml.unquote(method.asScalar().value())) - ), - mapping.yamlSequence(method).values().stream().map( - node -> node.asScalar().value() - ).collect(Collectors.toList()) - ); - } - } - return res; - } - - /** - * Removes extra quotes. - * @param val Value from yaml - * @return Unquoted string - */ - private static String unquote(final String val) { - return val.replaceAll("^[\"']", "").replaceAll("[\"']$", ""); - } - } -} diff --git a/src/main/java/com/artipie/front/auth/ApiTokens.java b/src/main/java/com/artipie/front/auth/ApiTokens.java deleted file mode 100644 index 8d8d412..0000000 --- a/src/main/java/com/artipie/front/auth/ApiTokens.java +++ /dev/null @@ -1,166 +0,0 @@ -/* - * The MIT License (MIT) Copyright (c) 2022 artipie.com - * https://github.com/artipie/front/LICENSE.txt - */ -package com.artipie.front.auth; - -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; -import java.time.Instant; -import java.util.Arrays; -import java.util.Random; -import org.apache.commons.codec.DecoderException; -import org.apache.commons.codec.binary.Hex; -import org.apache.commons.codec.digest.HmacAlgorithms; -import org.apache.commons.codec.digest.HmacUtils; - -/** - * Tokens for API. - *

- * This class generates tokens for user with specified - * expiration time. User name and expiration time are - * embedded into token value, then added randome 4-byte nonce - * and then 20-byte HMAC (SHA1) based on server key. - * Token structure is: first byte is user name length (user name max 256 bytes), - * then user name UTF-8 encoded string, then 4 byte of expiration time - * epoch seconds, then 4 bytes nonce, then 20 bytes HMAC. - * This class provides API for generating and - * validating token. Also, it contains helper object to parse - * token back. - *

- * @since 1.0 - * @checkstyle MagicNumberCheck (500 lines) - * @checkstyle TrailingCommentCheck (500 lines) - */ -public final class ApiTokens { - - /** - * HMAC key. - */ - private final byte[] key; - - /** - * Random number generator. - */ - private final Random rng; - - /** - * New tokens object. - * @param key HMAC key - * @param rng Random number generator - */ - public ApiTokens(final byte[] key, final Random rng) { - this.key = Arrays.copyOf(key, key.length); - this.rng = rng; - } - - /** - * Generate new token for user. - * @param user User ID - * @param expiration Token expiration time - * @return HEX encoded token with HMAC - */ - public String token(final String user, final Instant expiration) { - final var ubin = user.getBytes(StandardCharsets.UTF_8); - if (ubin.length > 256) { - throw new IllegalStateException("user name is too long"); - } - final byte[] nonce = new byte[4]; - this.rng.nextBytes(nonce); - final var buf = ByteBuffer.allocate(1 + ubin.length + 8 + 20); - buf.put((byte) ubin.length); - buf.put(ubin); - buf.putInt((int) (expiration.toEpochMilli() / 1000)); - buf.put(nonce); - final var dup = buf.duplicate(); - dup.rewind(); - dup.limit(dup.limit() - 20); - final byte[] hmac = new HmacUtils(HmacAlgorithms.HMAC_SHA_1, this.key).hmac(dup); - buf.put(hmac); - buf.rewind(); - return Hex.encodeHexString(buf); - } - - /** - * Validate token. - * @param token Token to be validated. - * @return True if HMAC is valid - * @checkstyle ReturnCountCheck (20 lines) - * @checkstyle MethodBodyCommentsCheck (20 lines) - */ - @SuppressWarnings("PMD.OnlyOneReturn") - public boolean validate(final String token) { - final byte[] bin; - try { - bin = Hex.decodeHex(token); - } catch (final DecoderException ignore) { - // token is not valid if hex string is malformed - return false; - } - final byte[] unsigned = new byte[bin.length - 20]; - final byte[] signature = new byte[20]; - System.arraycopy(bin, 0, unsigned, 0, unsigned.length); - System.arraycopy(bin, unsigned.length, signature, 0, 20); - final byte[] hmac = new HmacUtils(HmacAlgorithms.HMAC_SHA_1, this.key).hmac(unsigned); - return Arrays.equals(hmac, signature); - } - - /** - * Token helper object. - * @since 1.0 - */ - public static final class Token { - - /** - * Token data. - */ - private final byte[] data; - - /** - * Private constructor. - * @param data Token data - */ - private Token(final byte[] data) { - this.data = Arrays.copyOf(data, data.length); - } - - /** - * Extract user ID from token. - * @return User ID string - */ - public String user() { - final int len = this.data[0] & 0xFF; // unsigned length byte - final byte[] bin = new byte[len]; - System.arraycopy(this.data, 1, bin, 0, len); - return new String(bin, StandardCharsets.UTF_8); - } - - /** - * Check if token was expired. - * @param now Current time - * @return True if token was expired - */ - public boolean expired(final Instant now) { - final int len = this.data[0] & 0xFF; // unsigned length byte - final byte[] bin = new byte[4]; - System.arraycopy(this.data, 1 + len, bin, 0, 4); - final int epoch = ByteBuffer.wrap(bin).getInt(); - return Instant.ofEpochMilli(epoch * 1000L).isBefore(now); - } - - /** - * Parse token from hex string. - * @param token Token string - * @return Token object - * @throws IllegalArgumentException if token is malformed - */ - @SuppressWarnings("PMD.ProhibitPublicStaticMethods") - public static Token parse(final String token) { - try { - return new Token(Hex.decodeHex(token)); - } catch (final DecoderException err) { - throw new IllegalArgumentException("invalid token", err); - } - } - } -} diff --git a/src/main/java/com/artipie/front/auth/AuthByPassword.java b/src/main/java/com/artipie/front/auth/AuthByPassword.java deleted file mode 100644 index 36ca11a..0000000 --- a/src/main/java/com/artipie/front/auth/AuthByPassword.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * The MIT License (MIT) Copyright (c) 2022 artipie.com - * https://github.com/artipie/front/LICENSE.txt - */ -package com.artipie.front.auth; - -import java.util.Optional; - -/** - * By password authentication. - * @since 1.0 - */ -@FunctionalInterface -public interface AuthByPassword { - - /** - * Authenticate user by password. - * @param user Login - * @param password Password - * @return User ID if found - */ - Optional authenticate(String user, String password); - - /** - * Authenticator with credentials. - * @param cred Credentials - * @return Authentication function - */ - @SuppressWarnings("PMD.ProhibitPublicStaticMethods") - static AuthByPassword withCredentials(final Credentials cred) { - return (name, pass) -> cred.user(name) - .filter(user -> user.validatePassword(pass)) - .map(User::uid); - } -} diff --git a/src/main/java/com/artipie/front/auth/Credentials.java b/src/main/java/com/artipie/front/auth/Credentials.java deleted file mode 100644 index 48d1f5b..0000000 --- a/src/main/java/com/artipie/front/auth/Credentials.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * The MIT License (MIT) Copyright (c) 2022 artipie.com - * https://github.com/artipie/front/LICENSE.txt - */ -package com.artipie.front.auth; - -import java.util.Arrays; -import java.util.Collection; -import java.util.Optional; - -/** - * Credentials. - * @since 1.0 - */ -public interface Credentials { - /** - * Find user by name. - * @param name Username - * @return User if found - */ - Optional user(String name); - - /** - * Reload or re-read credentials. Depending on implementation, this - * method can do nothing. - */ - void reload(); - - /** - * Decorator of {@link Credentials}, which tries to find user among several - * {@link Credentials} implementations, returns any user if found, empty optional - * if user not found. - * @since 0.2 - */ - class Any implements Credentials { - - /** - * Origins list. - */ - private final Collection creds; - - /** - * Primary ctor. - * @param creds Origins - */ - public Any(final Collection creds) { - this.creds = creds; - } - - /** - * Ctor. - * @param creds Origins - */ - public Any(final Credentials... creds) { - this(Arrays.asList(creds)); - } - - @Override - public Optional user(final String name) { - return this.creds.stream().filter(item -> item.user(name).isPresent()).findFirst() - .flatMap(item -> item.user(name)); - } - - @Override - public void reload() { - this.creds.forEach(Credentials::reload); - } - } - -} diff --git a/src/main/java/com/artipie/front/auth/EnvCredentials.java b/src/main/java/com/artipie/front/auth/EnvCredentials.java deleted file mode 100644 index 2ccca80..0000000 --- a/src/main/java/com/artipie/front/auth/EnvCredentials.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * The MIT License (MIT) Copyright (c) 2022 artipie.com - * https://github.com/artipie/front/LICENSE.txt - */ -package com.artipie.front.auth; - -import java.util.Collections; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; - -/** - * Credentials from env. - * @since 0.2 - */ -public final class EnvCredentials implements Credentials { - - /** - * Environment name for user. - */ - public static final String ENV_NAME = "ARTIPIE_USER_NAME"; - - /** - * Environment name for password. - */ - private static final String ENV_PASS = "ARTIPIE_USER_PASS"; - - /** - * Environment variables. - */ - private final Map env; - - /** - * Ctor. - * @param env Environment - */ - public EnvCredentials(final Map env) { - this.env = env; - } - - /** - * Default ctor with system environment. - */ - public EnvCredentials() { - this(System.getenv()); - } - - @Override - public Optional user(final String name) { - Optional result = Optional.empty(); - if (this.env.get(EnvCredentials.ENV_NAME) != null - && this.env.get(EnvCredentials.ENV_PASS) == null) { - throw new IllegalStateException( - // @checkstyle LineLengthCheck (1 line) - "Password is not set: env variable `ARTIPIE_USER_PASS` is required for env credentials" - ); - } else if ( - Objects.equals(Objects.requireNonNull(name), this.env.get(EnvCredentials.ENV_NAME)) - ) { - result = Optional.of( - // @checkstyle AnonInnerLengthCheck (30 lines) - new User() { - @Override - public boolean validatePassword(final String pass) { - return Objects.equals( - pass, EnvCredentials.this.env.get(EnvCredentials.ENV_PASS) - ); - } - - @Override - public String uid() { - return name; - } - - @Override - public Set groups() { - return Collections.emptySet(); - } - - @Override - public Optional email() { - return Optional.empty(); - } - } - ); - } - return result; - } - - @Override - public void reload() { - // does nothing - } -} diff --git a/src/main/java/com/artipie/front/auth/GithubCredentials.java b/src/main/java/com/artipie/front/auth/GithubCredentials.java deleted file mode 100644 index e641cbb..0000000 --- a/src/main/java/com/artipie/front/auth/GithubCredentials.java +++ /dev/null @@ -1,135 +0,0 @@ -/* - * The MIT License (MIT) Copyright (c) 2022 artipie.com - * https://github.com/artipie/front/LICENSE.txt - */ -package com.artipie.front.auth; - -import com.artipie.ArtipieException; -import com.jcabi.github.RtGithub; -import java.io.IOException; -import java.util.Collections; -import java.util.Locale; -import java.util.Optional; -import java.util.Set; -import java.util.function.Function; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * GitHub authentication uses username prefixed by provider name {@code github.com} - * and personal access token as a password. - * See GitHub docs - * for details. - * @implNote This implementation is not case sensitive. - * @since 0.2 - */ -public final class GithubCredentials implements Credentials { - - /** - * Username pattern, starts with provider name {@code github.com}, slash, - * and GitHub username, e.g. {@code github.com/octocat}. - */ - private static final Pattern PTN_NAME = Pattern.compile("^github\\.com/(.+)$"); - - /** - * Github username resolver by personal access token. - */ - private final Function github; - - /** - * New GitHub authentication. - * @checkstyle ReturnCountCheck (10 lines) - */ - public GithubCredentials() { - this( - token -> { - try { - return new RtGithub(token).users().self().login(); - } catch (final IOException unauthorized) { - return ""; - } - } - ); - } - - /** - * Primary constructor. - * @param github Resolves GitHub token to username - */ - GithubCredentials(final Function github) { - this.github = github; - } - - @Override - public Optional user(final String username) { - Optional result = Optional.empty(); - final Matcher matcher = GithubCredentials.PTN_NAME.matcher(username); - if (matcher.matches()) { - result = Optional.of(new GithubUser(matcher.group(1))); - } - return result; - } - - @Override - public void reload() { - // does nothing - } - - @Override - public String toString() { - return String.format("%s()", this.getClass().getSimpleName()); - } - - /** - * Implementation of {@link User} for github user. - * @since 0.2 - */ - class GithubUser implements User { - - /** - * Username. - */ - private final String uname; - - /** - * Ctor. - * @param uname Name of the user - */ - GithubUser(final String uname) { - this.uname = uname; - } - - @Override - public boolean validatePassword(final String pass) { - boolean res = false; - try { - final String login = GithubCredentials.this.github.apply(pass) - .toLowerCase(Locale.US); - if (login.equalsIgnoreCase(this.uname)) { - res = true; - } - } catch (final AssertionError error) { - if (error.getMessage() == null - || !error.getMessage().contains("401 Unauthorized")) { - throw new ArtipieException(error); - } - } - return res; - } - - @Override - public String uid() { - return this.uname; - } - - @Override - public Set groups() { - return Collections.emptySet(); - } - - @Override - public Optional email() { - return Optional.empty(); - } - } -} diff --git a/src/main/java/com/artipie/front/auth/User.java b/src/main/java/com/artipie/front/auth/User.java deleted file mode 100644 index ade7719..0000000 --- a/src/main/java/com/artipie/front/auth/User.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * The MIT License (MIT) Copyright (c) 2022 artipie.com - * https://github.com/artipie/front/LICENSE.txt - */ -package com.artipie.front.auth; - -import java.util.Optional; -import java.util.Set; - -/** - * User. - * - * @since 1.0 - */ -public interface User { - - /** - * Validate user password. - * - * @param pass Password to check - * @return True if password is valid - */ - boolean validatePassword(String pass); - - /** - * User id (name). - * - * @return String id - */ - String uid(); - - /** - * User groups. - * - * @return Readonly set of groups - */ - Set groups(); - - /** - * User email. - * - * @return Email if present, empty otherwise - */ - Optional email(); - -} diff --git a/src/main/java/com/artipie/front/auth/UserPermissions.java b/src/main/java/com/artipie/front/auth/UserPermissions.java deleted file mode 100644 index f60b1ea..0000000 --- a/src/main/java/com/artipie/front/auth/UserPermissions.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * The MIT License (MIT) Copyright (c) 2022 artipie.com - * https://github.com/artipie/front/LICENSE.txt - */ -package com.artipie.front.auth; - -import com.amihaiemil.eoyaml.YamlMapping; -import java.util.Optional; - -/** - * User authorization permissions. - * @since 1.0 - */ -public interface UserPermissions { - - /** - * Stub permissions for development and debugging. - * Remove after actual implementation. - */ - UserPermissions STUB = (uid, perm) -> true; - - /** - * Check if permissions is allowed for user. - * @param uid User ID - * @param perm Permission name - * @return True if allowed - */ - boolean allowed(String uid, String perm); - - /** - * Permissions from yaml. - * @since 0.1 - */ - final class FromYaml implements UserPermissions { - - /** - * Users permissions yaml. - */ - private final YamlMapping yaml; - - /** - * Ctor. - * @param yaml Users permissions yaml - */ - public FromYaml(final YamlMapping yaml) { - this.yaml = yaml; - } - - @Override - public boolean allowed(final String uid, final String perm) { - return Optional.ofNullable(this.yaml.yamlSequence(uid)) - .map( - seq -> seq.values().stream().map(item -> item.asScalar().value()) - .anyMatch(perm::equals) - ).orElse(false); - } - } -} diff --git a/src/main/java/com/artipie/front/auth/Users.java b/src/main/java/com/artipie/front/auth/Users.java deleted file mode 100644 index f34d615..0000000 --- a/src/main/java/com/artipie/front/auth/Users.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * The MIT License (MIT) Copyright (c) 2022 artipie.com - * https://github.com/artipie/front/LICENSE.txt - */ -package com.artipie.front.auth; - -import java.util.Collection; -import javax.json.JsonObject; - -/** - * Artipie users. - * @since 0.1 - */ -public interface Users { - - /** - * List existing users. - * @return Artipie users - */ - Collection list(); - - /** - * Add user. - * @param info User info (password, email, groups, etc) - * @param uid User name - */ - void add(JsonObject info, String uid); - - /** - * Remove user by name. - * @param uid User name - * @throws com.artipie.front.api.NotFoundException If user does not exist - */ - void remove(String uid); - -} diff --git a/src/main/java/com/artipie/front/auth/YamlCredentials.java b/src/main/java/com/artipie/front/auth/YamlCredentials.java deleted file mode 100644 index 0f081ea..0000000 --- a/src/main/java/com/artipie/front/auth/YamlCredentials.java +++ /dev/null @@ -1,189 +0,0 @@ -/* - * The MIT License (MIT) Copyright (c) 2022 artipie.com - * https://github.com/artipie/front/LICENSE.txt - */ -package com.artipie.front.auth; - -import com.amihaiemil.eoyaml.Yaml; -import com.amihaiemil.eoyaml.YamlMapping; -import com.artipie.asto.Key; -import com.artipie.asto.blocking.BlockingStorage; -import com.artipie.asto.misc.UncheckedScalar; -import com.google.common.cache.CacheBuilder; -import com.google.common.cache.CacheLoader; -import com.google.common.cache.LoadingCache; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.nio.charset.StandardCharsets; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; -import org.apache.commons.codec.digest.DigestUtils; - -/** - * Yaml credentials parser. - * - * @since 1.0 - */ -public final class YamlCredentials implements Credentials { - - /** - * Empty yaml constant. - */ - private static final YamlMapping EMPTY_YAML = Yaml.createYamlMappingBuilder().build(); - - /** - * Cache for credentials settings. - */ - private final LoadingCache creds; - - /** - * New yaml credentials with default ttl 60 seconds. - * - * @param asto Blocking storage - * @param key Credentials key - */ - public YamlCredentials(final BlockingStorage asto, final Key key) { - // @checkstyle MagicNumberCheck (1 line) - this(asto, key, 60); - } - - /** - * New yaml credentials. - * - * @param asto Blocking storage - * @param key Credentials key - * @param ttl Time to live in seconds - */ - public YamlCredentials(final BlockingStorage asto, final Key key, final int ttl) { - this.creds = CacheBuilder.newBuilder() - .expireAfterWrite(ttl, TimeUnit.SECONDS) - .build( - new CacheLoader<>() { - @Override - public YamlMapping load(final String name) { - final YamlMapping res; - if (asto.exists(key)) { - try { - res = Yaml.createYamlInput( - new String(asto.value(key), StandardCharsets.UTF_8) - ).readYamlMapping(); - } catch (final IOException err) { - throw new UncheckedIOException(err); - } - } else { - res = YamlCredentials.EMPTY_YAML; - } - return res; - } - } - ); - } - - @Override - public Optional user(final String name) { - return Optional.ofNullable( - new UncheckedScalar<>(() -> this.creds.get("any")).value().yamlMapping("credentials") - ) - .flatMap(cred -> Optional.ofNullable(cred.yamlMapping(name))) - .map(yaml -> new YamlUser(yaml, name)); - } - - @Override - public void reload() { - this.creds.invalidateAll(); - } - - /** - * Yaml user item. - * @since 1.0 - */ - public static final class YamlUser implements User { - - /** - * Yaml source. - */ - private final YamlMapping mapping; - - /** - * User name (id). - */ - private final String name; - - /** - * New user. - * @param mapping Yaml - * @param name User name - */ - public YamlUser(final YamlMapping mapping, final String name) { - this.mapping = mapping; - this.name = name; - } - - @Override - public boolean validatePassword(final String pass) { - final var config = this.mapping.string("pass"); - if (config == null) { - throw new IllegalStateException( - "invalid credentials configuration: `pass` field not found" - ); - } - final var type = this.mapping.string("type"); - final boolean res; - if (type == null) { - res = validateStringPass(config, pass); - } else { - res = validateStringPass(String.join(":", type, config), pass); - } - return res; - } - - @Override - public String uid() { - return this.name; - } - - @Override - public Set groups() { - return Optional.ofNullable(this.mapping.yamlSequence("groups")) - .map(seq -> StreamSupport.stream(seq.spliterator(), false)) - .orElse(Stream.empty()) - .map(node -> node.asScalar().value()) - .collect(Collectors.toSet()); - } - - @Override - public Optional email() { - return Optional.ofNullable(this.mapping.string("email")); - } - - /** - * Validate password string. - * @param config Passowrd string config - * @param pass Actual password - * @return True if password is valid - * @checkstyle ReturnCountCheck (30 lines) - */ - @SuppressWarnings("PMD.OnlyOneReturn") - private static boolean validateStringPass(final String config, final String pass) { - final var parts = config.split(":"); - switch (parts[0]) { - case "plain": - return Objects.equals(parts[1], pass); - case "sha256": - return Objects.equals(parts[1], DigestUtils.sha256Hex(pass)); - default: - throw new IllegalStateException( - String.format( - "invalid credentials configuration: `pass` type `%s` is not supported", - parts[0] - ) - ); - } - } - } -} diff --git a/src/main/java/com/artipie/front/auth/YamlUsers.java b/src/main/java/com/artipie/front/auth/YamlUsers.java deleted file mode 100644 index cb05471..0000000 --- a/src/main/java/com/artipie/front/auth/YamlUsers.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * The MIT License (MIT) Copyright (c) 2022 artipie.com - * https://github.com/artipie/front/LICENSE.txt - */ -package com.artipie.front.auth; - -import com.amihaiemil.eoyaml.Yaml; -import com.amihaiemil.eoyaml.YamlMapping; -import com.amihaiemil.eoyaml.YamlMappingBuilder; -import com.amihaiemil.eoyaml.YamlNode; -import com.artipie.asto.Key; -import com.artipie.asto.blocking.BlockingStorage; -import com.artipie.front.api.NotFoundException; -import com.artipie.front.misc.Json2Yaml; -import com.artipie.front.settings.ArtipieYaml; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.nio.charset.StandardCharsets; -import java.util.Collection; -import java.util.Collections; -import java.util.Optional; -import java.util.stream.Collectors; -import javax.json.JsonObject; - -/** - * Users from yaml file. - * - * @since 0.1 - */ -public final class YamlUsers implements Users { - - /** - * Yaml file key. - */ - private final Key key; - - /** - * Storage. - */ - private final BlockingStorage blsto; - - /** - * Ctor. - * - * @param key Yaml file key - * @param blsto Storage - */ - public YamlUsers(final Key key, final BlockingStorage blsto) { - this.key = key; - this.blsto = blsto; - } - - @Override - public Collection list() { - final Optional users = this.users(); - return users.map( - yaml -> yaml.keys().stream() - .map(node -> node.asScalar().value()).map( - name -> new YamlCredentials.YamlUser( - users.get().yamlMapping(name), - name - ) - ).collect(Collectors.toList()) - ).orElse(Collections.emptyList()); - } - - @Override - public void add(final JsonObject info, final String uid) { - YamlMappingBuilder builder = Yaml.createYamlMappingBuilder(); - final Optional users = this.users(); - if (users.isPresent()) { - for (final YamlNode node : users.get().keys()) { - final String val = node.asScalar().value(); - builder = builder.add(val, users.get().yamlMapping(val)); - } - } - builder = builder.add(uid, new Json2Yaml().apply(info.toString())); - this.blsto.save( - this.key, - Yaml.createYamlMappingBuilder().add(ArtipieYaml.NODE_CREDENTIALS, builder.build()) - .build().toString().getBytes(StandardCharsets.UTF_8) - ); - } - - @Override - public void remove(final String uid) { - if (this.users().map(yaml -> yaml.yamlMapping(uid) != null).orElse(false)) { - YamlMappingBuilder builder = Yaml.createYamlMappingBuilder(); - final YamlMapping users = this.users().get(); - for (final YamlNode node : users.keys()) { - final String val = node.asScalar().value(); - if (!uid.equals(val)) { - builder = builder.add(val, users.yamlMapping(val)); - } - } - this.blsto.save( - this.key, - Yaml.createYamlMappingBuilder() - .add(ArtipieYaml.NODE_CREDENTIALS, builder.build()) - .build().toString().getBytes(StandardCharsets.UTF_8) - ); - return; - } - throw new NotFoundException(String.format("User %s does not exist", uid)); - } - - /** - * Read yaml mapping with users from yaml file. - * - * @return Users yaml mapping - */ - private Optional users() { - Optional res = Optional.empty(); - if (this.blsto.exists(this.key)) { - try { - res = Optional.ofNullable( - Yaml.createYamlInput( - new String(this.blsto.value(this.key), StandardCharsets.UTF_8) - ).readYamlMapping().yamlMapping("credentials") - ); - } catch (final IOException err) { - throw new UncheckedIOException(err); - } - } - return res; - } -} diff --git a/src/main/java/com/artipie/front/auth/package-info.java b/src/main/java/com/artipie/front/auth/package-info.java deleted file mode 100644 index 339cfaf..0000000 --- a/src/main/java/com/artipie/front/auth/package-info.java +++ /dev/null @@ -1,10 +0,0 @@ -/* - * The MIT License (MIT) Copyright (c) 2022 artipie.com - * https://github.com/artipie/front/LICENSE.txt - */ - -/** - * AUthentication and authorization. - * @since 1.0 - */ -package com.artipie.front.auth; diff --git a/src/main/java/com/artipie/front/rest/AuthService.java b/src/main/java/com/artipie/front/rest/AuthService.java new file mode 100644 index 0000000..f638078 --- /dev/null +++ b/src/main/java/com/artipie/front/rest/AuthService.java @@ -0,0 +1,49 @@ +/* + * The MIT License (MIT) Copyright (c) 2022 artipie.com + * https://github.com/artipie/front/LICENSE.txt + */ +package com.artipie.front.rest; + +import java.util.Optional; +import javax.json.Json; + +/** + * Auth-service. + * + * @since 1.0 + */ +public class AuthService extends BaseService { + /** + * Path to rest-api. + */ + private static final String TOKEN_PATH = "/api/v1/oauth/token"; + + /** + * Ctor. + * @param rest Artipie rest endpoint. + */ + public AuthService(final String rest) { + super(rest); + } + + /** + * Obtain JWT-token from auth rest-service. + * @param name User name. + * @param password User password. + * @return JWT-token. + */ + public String getJwtToken(final String name, final String password) { + return BaseService.handle( + this.httpPost( + Optional.empty(), + AuthService.TOKEN_PATH, + () -> + Json.createObjectBuilder() + .add("name", name) + .add("pass", password) + .build().toString() + ), + res -> BaseService.jsonObject(res).getString("token") + ); + } +} diff --git a/src/main/java/com/artipie/front/rest/BaseService.java b/src/main/java/com/artipie/front/rest/BaseService.java new file mode 100644 index 0000000..ee67ff3 --- /dev/null +++ b/src/main/java/com/artipie/front/rest/BaseService.java @@ -0,0 +1,344 @@ +/* + * The MIT License (MIT) Copyright (c) 2022 artipie.com + * https://github.com/artipie/front/LICENSE.txt + */ +package com.artipie.front.rest; + +import com.artipie.ArtipieException; +import com.artipie.front.RestException; +import com.artipie.front.misc.Json2Yaml; +import com.google.common.net.HttpHeaders; +import java.io.IOException; +import java.io.StringReader; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Supplier; +import javax.json.Json; +import javax.json.JsonArray; +import javax.json.JsonObject; +import javax.servlet.http.HttpServletResponse; + +/** + * Base rest-service. + * @since 1.0 + */ +@SuppressWarnings({"PMD.DataClass", "PMD.AvoidFieldNameMatchingMethodName", "PMD.TooManyMethods"}) +public class BaseService { + /** + * Application json content-type value. + */ + public static final String APPLICATION_JSON = "application/json"; + + /** + * Http request timeout. + */ + private static final Duration TIMEOUT = Duration.of(1, ChronoUnit.MINUTES); + + /** + * Artipie rest URL. + */ + private final String rest; + + /** + * Ctor. + * @param rest Artipie rest endpoint. + */ + public BaseService(final String rest) { + this.rest = rest; + } + + /** + * Gets artipie rest URL. + * @return Artipie rest URL. + */ + protected String rest() { + return this.rest; + } + + /** + * Gets uri to artipie rest resource. + * @param path Absolute path to rest resource. + * @return Artipie rest resource URI. + * @throws URISyntaxException if there is syntax error. + */ + protected URI uri(final String path) throws URISyntaxException { + return new URI(String.format("%s/%s", this.rest(), path)); + } + + /** + * Bearer for authorization header. + * @param token Token. + * @return Bearer for authorization header + */ + protected static String bearer(final String token) { + return String.format("Bearer %s", token); + } + + /** + * Invokes GET http request. + * @param token JWT token. + * @param path Path in URL. + * @return Http response. + */ + protected HttpResponse httpGet(final Optional token, final String path) { + try { + return HttpClient.newBuilder() + .build() + .send( + this.createGetRequest(token, path), + HttpResponse.BodyHandlers.ofString() + ); + } catch (final IOException | InterruptedException exc) { + throw new ArtipieException(exc); + } + } + + /** + * Invokes POST http request. + * @param token JWT token. + * @param path Path in URL. + * @param payload Payload supplier. + * @return Http response. + */ + protected HttpResponse httpPost(final Optional token, final String path, + final Supplier payload) { + try { + return HttpClient.newBuilder() + .build() + .send( + this.createPostRequest(token, path, payload), + HttpResponse.BodyHandlers.ofString() + ); + } catch (final IOException | InterruptedException exc) { + throw new ArtipieException(exc); + } + } + + /** + * Invokes POST http request. + * @param token JWT token. + * @param path Path in URL. + * @param payload Payload supplier. + * @return Http response. + */ + protected HttpResponse httpPut(final Optional token, final String path, + final Supplier payload) { + try { + return HttpClient.newBuilder() + .build() + .send( + this.createPutRequest(token, path, payload), + HttpResponse.BodyHandlers.ofString() + ); + } catch (final IOException | InterruptedException exc) { + throw new ArtipieException(exc); + } + } + + /** + * Invokes DELETE http request. + * @param token JWT token. + * @param path Path in URL. + * @return Http response. + */ + protected HttpResponse httpDelete(final Optional token, final String path) { + try { + return HttpClient.newBuilder() + .build() + .send( + this.createDeleteRequest(token, path), + HttpResponse.BodyHandlers.ofString() + ); + } catch (final IOException | InterruptedException exc) { + throw new ArtipieException(exc); + } + } + + /** + * Creates GET http request. + * @param token JWT token. + * @param path Path in URL. + * @return Http request. + */ + protected HttpRequest createGetRequest(final Optional token, final String path) { + try { + final HttpRequest.Builder builder = HttpRequest.newBuilder() + .uri(this.uri(path)) + .GET() + .header(HttpHeaders.ACCEPT, BaseService.APPLICATION_JSON) + .timeout(BaseService.TIMEOUT); + token.ifPresent(value -> builder.header(HttpHeaders.AUTHORIZATION, bearer(value))); + return builder.build(); + } catch (final URISyntaxException exc) { + throw new ArtipieException(exc); + } + } + + /** + * Creates POST http request. + * @param token JWT token. + * @param path Path in URL. + * @param payload Payload supplier. + * @return Http request. + */ + protected HttpRequest createPostRequest(final Optional token, final String path, + final Supplier payload) { + try { + final HttpRequest.Builder builder = HttpRequest.newBuilder() + .uri(this.uri(path)) + .POST(HttpRequest.BodyPublishers.ofString(payload.get())) + .header(HttpHeaders.ACCEPT, BaseService.APPLICATION_JSON) + .header(HttpHeaders.CONTENT_TYPE, BaseService.APPLICATION_JSON) + .timeout(BaseService.TIMEOUT); + token.ifPresent(value -> builder.header(HttpHeaders.AUTHORIZATION, bearer(value))); + return builder.build(); + } catch (final URISyntaxException exc) { + throw new ArtipieException(exc); + } + } + + /** + * Creates PUT http request. + * @param token JWT token. + * @param path Path in URL. + * @param payload Payload supplier. + * @return Http request. + */ + protected HttpRequest createPutRequest(final Optional token, final String path, + final Supplier payload) { + try { + final HttpRequest.Builder builder = HttpRequest.newBuilder() + .uri(this.uri(path)) + .PUT(HttpRequest.BodyPublishers.ofString(payload.get())) + .header(HttpHeaders.ACCEPT, BaseService.APPLICATION_JSON) + .header(HttpHeaders.CONTENT_TYPE, BaseService.APPLICATION_JSON) + .timeout(BaseService.TIMEOUT); + token.ifPresent(value -> builder.header(HttpHeaders.AUTHORIZATION, bearer(value))); + return builder.build(); + } catch (final URISyntaxException exc) { + throw new ArtipieException(exc); + } + } + + /** + * Creates DELETE http request. + * @param token JWT token. + * @param path Path in URL. + * @return Http request. + */ + protected HttpRequest createDeleteRequest(final Optional token, final String path) { + try { + final HttpRequest.Builder builder = HttpRequest.newBuilder() + .uri(this.uri(path)) + .DELETE() + .header(HttpHeaders.ACCEPT, BaseService.APPLICATION_JSON) + .timeout(BaseService.TIMEOUT); + token.ifPresent(value -> builder.header(HttpHeaders.AUTHORIZATION, bearer(value))); + return builder.build(); + } catch (final URISyntaxException exc) { + throw new ArtipieException(exc); + } + } + + /** + * Reads response body as json-object. + * @param response Response. + * @return JsonObject. + */ + protected static JsonObject jsonObject(final HttpResponse response) { + return Json.createReader(new StringReader(response.body())).readObject(); + } + + /** + * Reads response body as json-array. + * @param response Response. + * @return JsonArray. + */ + protected static JsonArray jsonArray(final HttpResponse response) { + return Json.createReader(new StringReader(response.body())).readArray(); + } + + /** + * Convert response json-body to yaml. + * @param response Response. + * @return Yaml content. + */ + protected static String toYaml(final HttpResponse response) { + return new Json2Yaml().apply(BaseService.jsonObject(response).toString()) + .toString(); + } + + /** + * Strip leading and ending quotes. + * @param str String with leading and ending quotes or without them. + * @return Stripped string. + */ + protected static String stripQuotes(final String str) { + String result = str; + if (str.length() > 1 && str.charAt(0) == '"' && str.charAt(str.length() - 1) == '"') { + result = str.substring(1, str.length() - 1); + } + return result; + } + + /** + * If status code is successful http response (200) + * then converts http-response by mapping function to content of specified type, + * Otherwise throws RestException with resulting status code and http response body. + * + * @param response Response. + * @param map Map-function that forms returning content in case expected status code. + * @param Type of resulting content of map-function. + * @return Content of specified type in case success result code. + * @throws RestException with response status code and response body + * in case unexpected status code. + */ + protected static V handle(final HttpResponse response, + final Function, V> map) throws RestException { + return handle(HttpServletResponse.SC_OK, response, map); + } + + /** + * If status code has expected value + * then converts http-response by mapping function to content of specified type, + * Otherwise throws RestException with resulting status code and http response body. + * + * @param success Expected success result code. + * @param response Response. + * @param map Map-function that forms returning content in case expected status code. + * @param Type of resulting content of map-function. + * @return Content of specified type in case success result code. + * @throws RestException with response status code and response body + * in case unexpected status code. + */ + protected static V handle(final int success, final HttpResponse response, + final Function, V> map) throws RestException { + if (success == response.statusCode()) { + return map.apply(response); + } + throw new RestException(response.statusCode(), response.body()); + } + + /** + * Join path-parts. + * @param parts Parts of path. + * @return Path as joined by '/'-symbol path-parts. + */ + protected static String path(final Object...parts) { + final StringBuilder builder = new StringBuilder(); + for (final Object path : parts) { + builder.append(path).append('/'); + } + if (builder.length() > 0) { + builder.setLength(builder.length() - 1); + } + return builder.toString(); + } +} diff --git a/src/main/java/com/artipie/front/rest/RepositoryName.java b/src/main/java/com/artipie/front/rest/RepositoryName.java new file mode 100644 index 0000000..d99c6b5 --- /dev/null +++ b/src/main/java/com/artipie/front/rest/RepositoryName.java @@ -0,0 +1,142 @@ +/* + * The MIT License (MIT) Copyright (c) 2022 artipie.com + * https://github.com/artipie/front/LICENSE.txt + */ +package com.artipie.front.rest; + +import com.artipie.front.Layout; +import spark.Request; + +/** + * Repository name. + * + * @since 1.0 + */ +public interface RepositoryName { + /** + * Repository name path parameter name. + */ + String REPO = ":repo"; + + /** + * The name of the repository. + * + * @return String name + */ + String toString(); + + /** + * Repository name from request. + * + * @since 1.0 + */ + class FromRequest implements RepositoryName { + /** + * Request. + */ + private final Request request; + + /** + * Layout. + */ + private final Layout layout; + + /** + * Ctor. + * + * @param request Request + * @param layout Layout + */ + public FromRequest(final Request request, final Layout layout) { + this.request = request; + this.layout = layout; + } + + /** + * Provides string representation of repository name in toString() method. + *
    + *
  • 'reponame' for flat layout
  • + *
  • 'username/reponame' for org layout
  • + *
+ * + * @checkstyle NoJavadocForOverriddenMethodsCheck (10 lines) + */ + @Override + public String toString() { + final String reponame; + if (this.layout == Layout.FLAT) { + reponame = new Flat( + this.request.params(RepositoryName.REPO) + ).toString(); + } else { + reponame = new Org( + this.request.params(RepositoryName.REPO), + this.request.session().attribute("uid") + ).toString(); + } + return reponame; + } + } + + /** + * Repository name for flat layout. + * + * @since 1.0 + */ + class Flat implements RepositoryName { + + /** + * Repository name. + */ + private final String repo; + + /** + * Ctor. + * + * @param repo Name + */ + public Flat(final String repo) { + this.repo = repo; + } + + @Override + public String toString() { + return this.repo; + } + } + + /** + * Repository name for org layout is combined from username and reponame: + * 'username/reponame'. + * + * @since 1.0 + */ + class Org implements RepositoryName { + + /** + * Repository name. + */ + private final String repo; + + /** + * User name. + */ + private final String user; + + /** + * Ctor. + * + * @param repo Repository name + * @param user User name + */ + public Org(final String repo, final String user) { + this.repo = repo; + this.user = user; + } + + @Override + public String toString() { + return String.format("%s/%s", this.user, this.repo); + } + } +} diff --git a/src/main/java/com/artipie/front/rest/RepositoryService.java b/src/main/java/com/artipie/front/rest/RepositoryService.java new file mode 100644 index 0000000..974f87f --- /dev/null +++ b/src/main/java/com/artipie/front/rest/RepositoryService.java @@ -0,0 +1,135 @@ +/* + * The MIT License (MIT) Copyright (c) 2022 artipie.com + * https://github.com/artipie/front/LICENSE.txt + */ +package com.artipie.front.rest; + +import com.artipie.front.misc.Yaml2Json; +import java.net.http.HttpResponse; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import javax.json.JsonArray; +import javax.json.JsonValue; + +/** + * Repository-service. + * + * @since 1.0 + */ +public class RepositoryService extends BaseService { + /** + * Path to 'repository'. + */ + private static final String REPOSITORY_PATH = "/api/v1/repository"; + + /** + * Path to 'list' rest-api. + */ + private static final String LIST_PATH = BaseService.path( + RepositoryService.REPOSITORY_PATH, "list" + ); + + /** + * Ctor. + * + * @param rest Artipie rest endpoint. + */ + public RepositoryService(final String rest) { + super(rest); + } + + /** + * Obtain list of repository names. + * @param token Token. + * @return List of repository names. + */ + public List list(final String token) { + return BaseService.handle( + this.httpGet(Optional.of(token), RepositoryService.LIST_PATH), + RepositoryService::listOfStrings + ); + } + + /** + * Obtain list of repository names by user's name. + * @param token Token. + * @param uname User name. + * @return List of repository names. + */ + public List list(final String token, final String uname) { + return BaseService.handle( + this.httpGet( + Optional.of(token), + BaseService.path(RepositoryService.LIST_PATH, uname) + ), + RepositoryService::listOfStrings + ); + } + + /** + * Obtain repository content. + * @param token Token. + * @param rname Repository name. + * @return Repository content. + */ + public String repo(final String token, final RepositoryName rname) { + return BaseService.handle( + this.httpGet( + Optional.of(token), + BaseService.path(RepositoryService.REPOSITORY_PATH, rname) + ), + BaseService::toYaml + ); + } + + /** + * Save repository config. + * @param token Token. + * @param rname Repository name. + * @param config Repository config. + * @return Resulting message + */ + public String save(final String token, final RepositoryName rname, + final String config) { + return BaseService.handle( + this.httpPut( + Optional.of(token), + BaseService.path(RepositoryService.REPOSITORY_PATH, rname), + () -> new Yaml2Json().apply(config).toString() + ), + res -> String.format("Repository %s saved successfully", rname) + ); + } + + /** + * Remove repository. + * @param token Token. + * @param rname Repository name. + * @return Resulting message + */ + public String remove(final String token, final RepositoryName rname) { + return BaseService.handle( + this.httpDelete( + Optional.of(token), + RepositoryService.path(RepositoryService.REPOSITORY_PATH, rname.toString()) + ), + res -> String.format("Repository %s removed", rname) + ); + } + + /** + * Reads response body and convert it to List of strings. + * Expects json-body as array of strings. + * @param res Response with json-body. + * @return List of string. + */ + private static List listOfStrings(final HttpResponse res) { + final JsonArray array = BaseService.jsonArray(res); + final List result = new ArrayList<>(array.size()); + for (final JsonValue item : array) { + result.add(BaseService.stripQuotes(item.toString())); + } + return result; + } +} diff --git a/src/main/java/com/artipie/front/settings/package-info.java b/src/main/java/com/artipie/front/rest/package-info.java similarity index 81% rename from src/main/java/com/artipie/front/settings/package-info.java rename to src/main/java/com/artipie/front/rest/package-info.java index cd42960..c52479a 100644 --- a/src/main/java/com/artipie/front/settings/package-info.java +++ b/src/main/java/com/artipie/front/rest/package-info.java @@ -7,4 +7,4 @@ * Artipie settings. * @since 0.1 */ -package com.artipie.front.settings; +package com.artipie.front.rest; diff --git a/src/main/java/com/artipie/front/settings/ArtipieYaml.java b/src/main/java/com/artipie/front/settings/ArtipieYaml.java deleted file mode 100644 index 7ad6beb..0000000 --- a/src/main/java/com/artipie/front/settings/ArtipieYaml.java +++ /dev/null @@ -1,224 +0,0 @@ -/* - * The MIT License (MIT) Copyright (c) 2022 artipie.com - * https://github.com/artipie/front/LICENSE.txt - */ -package com.artipie.front.settings; - -import com.amihaiemil.eoyaml.Yaml; -import com.amihaiemil.eoyaml.YamlMapping; -import com.amihaiemil.eoyaml.YamlNode; -import com.artipie.ArtipieException; -import com.artipie.asto.Key; -import com.artipie.asto.Storage; -import com.artipie.asto.SubStorage; -import com.artipie.asto.blocking.BlockingStorage; -import com.artipie.front.auth.AccessPermissions; -import com.artipie.front.auth.Credentials; -import com.artipie.front.auth.EnvCredentials; -import com.artipie.front.auth.GithubCredentials; -import com.artipie.front.auth.UserPermissions; -import com.artipie.front.auth.Users; -import com.artipie.front.auth.YamlCredentials; -import com.artipie.front.auth.YamlUsers; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -/** - * Artipie yaml settings. - * @since 0.1 - * @checkstyle ClassDataAbstractionCouplingCheck (500 lines) - */ -@SuppressWarnings("PMD.TooManyMethods") -public final class ArtipieYaml { - - /** - * Yaml node credentials name. - */ - public static final String NODE_CREDENTIALS = "credentials"; - - /** - * Yaml node type. - */ - private static final String NODE_TYPE = "type"; - - /** - * YAML file content. - */ - private final YamlMapping content; - - /** - * Ctor. - * @param content YAML file content - */ - public ArtipieYaml(final YamlMapping content) { - this.content = content; - } - - /** - * Artipie storage. - * @return Storage - */ - public BlockingStorage storage() { - return new BlockingStorage(this.asto()); - } - - /** - * Artipie layout: flat or org. - * @return Layout value - */ - public String layout() { - return Optional.ofNullable(this.meta().string("layout")).orElse("flat"); - } - - /** - * Yaml settings meta section. - * @return Meta yaml section - */ - public YamlMapping meta() { - return this.content.yamlMapping("meta"); - } - - /** - * Repository configurations storage. - * @return Storage, where repo configs are located - */ - public BlockingStorage repoConfigsStorage() { - return new BlockingStorage( - Optional.ofNullable(this.meta().string("repo_configs")) - .map(str -> new SubStorage(new Key.From(str), this.asto())) - .orElse(this.asto()) - ); - } - - /** - * Artipie users. - * @return Users instance - */ - public Users users() { - return new YamlUsers( - this.fileCredentialsKey().orElse(new Key.From("_credentials.yml")), this.storage() - ); - } - - /** - * Credentials from config. - * @return Credentials - */ - public Credentials credentials() { - final List res = new ArrayList<>(3); - this.yamlCredentials().ifPresent(res::add); - if (this.credentialsTypeSet("env")) { - res.add(new EnvCredentials()); - } - if (this.credentialsTypeSet("github")) { - res.add(new GithubCredentials()); - } - return new Credentials.Any(res); - } - - /** - * Read access permissions. - * @return Instance of {@link AccessPermissions} - */ - public AccessPermissions accessPermissions() { - return this.apiPermissions("endpoints") - .map(AccessPermissions.FromYaml::new) - .orElse(AccessPermissions.STUB); - } - - /** - * Read users api permissions. - * @return Instance of {@link UserPermissions} - */ - public UserPermissions userPermissions() { - return this.apiPermissions("users") - .map(UserPermissions.FromYaml::new) - .orElse(UserPermissions.STUB); - } - - @Override - public String toString() { - return String.format("YamlSettings{\n%s\n}", this.content.toString()); - } - - /** - * Abstract storage. - * @return Asto - */ - private Storage asto() { - return new YamlStorage( - Optional.ofNullable( - this.meta().yamlMapping("storage") - ).orElseThrow( - () -> new ArtipieException( - String.format( - "Failed to find storage configuration in \n%s", this.content.toString() - ) - ) - ) - ).storage(); - } - - /** - * File credentials key (=storage relative path) if set. - * @return Key to credentials file. - */ - private Optional fileCredentialsKey() { - return Optional.ofNullable( - this.meta().yamlSequence(ArtipieYaml.NODE_CREDENTIALS) - ).map( - seq -> seq.values().stream() - .filter(node -> "file".equals(node.asMapping().string(ArtipieYaml.NODE_TYPE))) - .findFirst().map(YamlNode::asMapping) - ).orElse(Optional.ofNullable(this.meta().yamlMapping(ArtipieYaml.NODE_CREDENTIALS))) - .map(file -> new Key.From(file.string("path"))); - } - - /** - * Obtain credentials from file yaml mapping if file credentials are set. - * @return Credentials if found - */ - private Optional yamlCredentials() { - return this.fileCredentialsKey().map(key -> new YamlCredentials(this.storage(), key)); - } - - /** - * Are credentials with given type set? - * @param type Credentials type - * @return True if configured - */ - private boolean credentialsTypeSet(final String type) { - return Optional.ofNullable( - this.meta().yamlSequence(ArtipieYaml.NODE_CREDENTIALS) - ).map( - seq -> seq.values().stream() - .anyMatch(node -> type.equals(node.asMapping().string(ArtipieYaml.NODE_TYPE))) - ).orElse(false); - } - - /** - * Read API permissions. - * @param section Yaml section to read - * @return Yaml mapping if the file exists - */ - private Optional apiPermissions(final String section) { - Optional res = Optional.empty(); - final Key key = new Key.From("_api_permissions.yml"); - if (this.storage().exists(key)) { - try { - res = Optional.of( - Yaml.createYamlInput( - new String(this.storage().value(key), StandardCharsets.UTF_8) - ).readYamlMapping().yamlMapping(section) - ); - } catch (final IOException err) { - throw new UncheckedIOException(err); - } - } - return res; - } -} diff --git a/src/main/java/com/artipie/front/settings/RepoData.java b/src/main/java/com/artipie/front/settings/RepoData.java deleted file mode 100644 index 2c7a059..0000000 --- a/src/main/java/com/artipie/front/settings/RepoData.java +++ /dev/null @@ -1,135 +0,0 @@ -/* - * The MIT License (MIT) Copyright (c) 2022 artipie.com - * https://github.com/artipie/front/LICENSE.txt - */ -package com.artipie.front.settings; - -import com.amihaiemil.eoyaml.Yaml; -import com.amihaiemil.eoyaml.YamlMapping; -import com.artipie.asto.Copy; -import com.artipie.asto.Key; -import com.artipie.asto.Storage; -import com.artipie.asto.SubStorage; -import com.jcabi.log.Logger; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.nio.charset.StandardCharsets; -import java.util.Optional; -import java.util.concurrent.CompletionStage; -import java.util.stream.Stream; - -/** - * Repository data management. - * @since 0.1 - */ -@SuppressWarnings("PMD.AvoidDuplicateLiterals") -public final class RepoData { - - /** - * Repository settings. - */ - private final RepoSettings stn; - - /** - * Ctor. - * @param stn Repository settings - */ - public RepoData(final RepoSettings stn) { - this.stn = stn; - } - - /** - * Remove data from the repository. - * @param name Repository name - * @param uid User name - * @return Completable action of the remove operation - */ - public CompletionStage remove(final String name, final String uid) { - final String repo = this.stn.name(name, uid); - return this.asto(name, uid).deleteAll(new Key.From(repo)).thenAccept( - nothing -> Logger.info(this, String.format("Removed data from repository %s", repo)) - ); - } - - /** - * Move data when repository is renamed: from location by the old name to location with - * new name. - * @param name Repository name - * @param uid User name - * @param nname New repository name - * @return Completable action of the remove operation - */ - public CompletionStage move(final String name, final String uid, final String nname) { - final Key repo = new Key.From(this.stn.name(name, uid)); - final String nrepo = this.stn.name(nname, uid); - final Storage asto = this.asto(name, uid); - return new SubStorage(repo, asto).list(Key.ROOT).thenCompose( - list -> new Copy(new SubStorage(repo, asto), list) - .copy(new SubStorage(new Key.From(nrepo), asto)) - ).thenCompose(nothing -> asto.deleteAll(new Key.From(repo))).thenAccept( - nothing -> - Logger.info(this, String.format("Moved data from repository %s to %s", repo, nrepo)) - ); - } - - /** - * Obtain storage from repository settings. - * @param name Repository name - * @param uid User name - * @return Abstract storage - * @throws UncheckedIOException On IO errors - */ - private Storage asto(final String name, final String uid) { - try { - final YamlMapping yaml = Yaml.createYamlInput( - new String(this.stn.value(name, uid), StandardCharsets.UTF_8) - ).readYamlMapping().yamlMapping("repo"); - YamlMapping res = yaml.yamlMapping("storage"); - if (res == null) { - res = this.storageYamlByAlias(name, uid, yaml.string("storage")); - } - return new YamlStorage(res).storage(); - } catch (final IOException err) { - throw new UncheckedIOException(err); - } - } - - /** - * Find storage settings by alias, considering two file extensions and two locations. - * @param name Repository name - * @param uid User name - * @param alias Storage settings yaml by alias - * @return Yaml storage settings found by provided alias - * @throws IllegalStateException If storage with given alias not found - * @throws UncheckedIOException On IO errors - * @checkstyle LineLengthCheck (2 lines) - */ - private YamlMapping storageYamlByAlias(final String name, final String uid, final String alias) { - final Key repo = new Key.From(this.stn.name(name, uid)); - final Key yml = new Key.From("_storage.yaml"); - final Key yaml = new Key.From("_storage.yml"); - Optional res = Optional.empty(); - final Optional location = Stream.of( - new Key.From(repo, yaml), new Key.From(repo, yml), - repo.parent().map(item -> new Key.From(item, yaml)).orElse(yaml), - repo.parent().map(item -> new Key.From(item, yml)).orElse(yml) - ).filter(this.stn.repoConfigsStorage()::exists).findFirst(); - if (location.isPresent()) { - try { - res = Optional.of( - Yaml.createYamlInput( - new String( - this.stn.repoConfigsStorage().value(location.get()), - StandardCharsets.UTF_8 - ) - ).readYamlMapping().yamlMapping("storages").yamlMapping(alias) - ); - } catch (final IOException err) { - throw new UncheckedIOException(err); - } - } - return res.orElseThrow( - () -> new IllegalStateException(String.format("Storage alias %s not found", alias)) - ); - } -} diff --git a/src/main/java/com/artipie/front/settings/RepoPermissions.java b/src/main/java/com/artipie/front/settings/RepoPermissions.java deleted file mode 100644 index 1bd55a0..0000000 --- a/src/main/java/com/artipie/front/settings/RepoPermissions.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * The MIT License (MIT) Copyright (c) 2022 artipie.com - * https://github.com/artipie/front/LICENSE.txt - */ -package com.artipie.front.settings; - -import javax.json.JsonArray; -import javax.json.JsonObject; -import javax.json.JsonStructure; - -/** - * This interface if meant to manage repository permissions. - * @since 0.1 - */ -public interface RepoPermissions { - - /** - * Read permissions of the repository. - * @param repo Repository name - * @return Permissions as json object - * @throws com.artipie.front.api.NotFoundException If repository does not exist - */ - JsonObject get(String repo); - - /** - * Add repository permissions for user. - * @param repo Repository name - * @param uid User id (name) - * @param perms Permissions to add - * @throws com.artipie.front.api.NotFoundException If repository does not exist - */ - void add(String repo, String uid, JsonArray perms); - - /** - * Removes all the permissions for repository from user. Does nothing if user does not - * have any permissions in the repository. - * @param repo Repository name - * @param uid User id (name) - * @throws com.artipie.front.api.NotFoundException If repository does not exist - */ - void delete(String repo, String uid); - - /** - * Patch repository permissions. - * @param repo Repository name - * @param perms New permissions - * @throws com.artipie.front.api.NotFoundException If repository does not exist - */ - void patch(String repo, JsonStructure perms); -} diff --git a/src/main/java/com/artipie/front/settings/RepoSettings.java b/src/main/java/com/artipie/front/settings/RepoSettings.java deleted file mode 100644 index 3827a47..0000000 --- a/src/main/java/com/artipie/front/settings/RepoSettings.java +++ /dev/null @@ -1,188 +0,0 @@ -/* - * The MIT License (MIT) Copyright (c) 2022 artipie.com - * https://github.com/artipie/front/LICENSE.txt - */ -package com.artipie.front.settings; - -import com.artipie.asto.Key; -import com.artipie.asto.blocking.BlockingStorage; -import com.artipie.front.api.NotFoundException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Optional; -import org.apache.commons.lang3.tuple.ImmutablePair; -import org.apache.commons.lang3.tuple.Pair; - -/** - * Repository settings. While searching for repo settings key or value, - * this class takes into account: - *

- * - artipie layout: when layout if flat, repo settings are stored in the storage root, - * when layout is org - in the username subdir: {uid}/{name.yaml} - *

- *

- * - .yaml or .yml extension, both are considered - *

- * @since 0.1 - */ -@SuppressWarnings("PMD.AvoidDuplicateLiterals") -public final class RepoSettings { - - /** - * Artipie layout. - */ - private final String layout; - - /** - * Repositories configuration storage. - */ - private final BlockingStorage repos; - - /** - * Ctor. - * @param layout Artipie layout - * @param repos Repositories settings storage - */ - public RepoSettings(final String layout, final BlockingStorage repos) { - this.layout = layout; - this.repos = repos; - } - - /** - * List existing repositories. - * @param uid User id (=name) - * @return List of the existing repositories - */ - public Collection list(final Optional uid) { - Key key = Key.ROOT; - if ("org".equals(this.layout) && uid.isPresent()) { - key = new Key.From(uid.get()); - } - final Collection res = new ArrayList<>(5); - for (final Key item : this.repos.list(key)) { - final String name = item.string(); - // @checkstyle BooleanExpressionComplexityCheck (5 lines) - if ((name.endsWith(".yaml") || name.endsWith(".yml")) - && !name.contains(YamlStorages.FILE_NAME) - && !name.contains("_permissions") - && !name.contains("_credentials")) { - res.add(name.replaceAll("\\.yaml|\\.yml", "")); - } - } - return res; - } - - /** - * Find repository settings key by repository name and username, throws exception - * if settings file is not found. - * @param name Repository name - * @param uid User id (=name) - * @return Repository settings - * @throws NotFoundException If such repository does not exist - */ - public Key key(final String name, final String uid) { - final Pair pair = this.keys(name, uid); - Key res = pair.getLeft(); - if (!this.repos.exists(res)) { - res = pair.getRight(); - if (!this.repos.exists(res)) { - throw new NotFoundException(String.format("Repository %s not found", name)); - } - } - return res; - } - - /** - * Checks if repository settings exists key by repository name and username. - * @param name Repository name - * @param uid User id (=name) - * @return True if found - */ - public boolean exists(final String name, final String uid) { - return this.repos.exists(this.keys(name, uid).getLeft()) - || this.repos.exists(this.keys(name, uid).getRight()); - } - - /** - * Find repository settings by repository name and username, throws exception - * if settings file is not found. - * @param name Repository name - * @param uid User id (=name) - * @return Repository settings - * @throws NotFoundException If such repository does not exist - */ - public byte[] value(final String name, final String uid) { - return this.repos.value(this.key(name, uid)); - } - - /** - * Saves repository settings to repository settings storage. - * @param name Repository name - * @param uid User id (=name) - * @param value Settings body - */ - public void save(final String name, final String uid, final byte[] value) { - this.repos.save(this.keys(name, uid).getRight(), value); - } - - /** - * Removes repository settings. - * @param name Repository name - * @param uid User id (=name) - * @throws NotFoundException If such repository does not exist - */ - public void delete(final String name, final String uid) { - this.repos.delete(this.key(name, uid)); - } - - /** - * Moves repository settings. - * @param name Repository name - * @param uid User id (=name) - * @param nname New name - * @throws NotFoundException If such repository does not exist - */ - public void move(final String name, final String uid, final String nname) { - this.repos.move(this.key(name, uid), this.keys(nname, uid).getRight()); - } - - /** - * Returns repository name, the same as settings file key, but without yaml extension. - * @param name Repository name - * @param uid User id (=name) - * @return String name - */ - public String name(final String name, final String uid) { - String res = name; - if (this.layout.equals("org")) { - res = String.format("%s/%s", uid, name); - } - return res; - } - - /** - * Repositories configuration storage. - * @return Repo configs {@link BlockingStorage} - */ - public BlockingStorage repoConfigsStorage() { - return this.repos; - } - - /** - * Returns a pair of keys, these keys are possible repository settings names. - * @param name Repository name - * @param uid User id (=name) - * @return Pair of keys - */ - private Pair keys(final String name, final String uid) { - String first = ""; - if (this.layout.equals("org")) { - first = String.format("%s/", uid); - } - return new ImmutablePair<>( - new Key.From(String.format("%s%s.yaml", first, name)), - new Key.From(String.format("%s%s.yml", first, name)) - ); - } - -} diff --git a/src/main/java/com/artipie/front/settings/Storages.java b/src/main/java/com/artipie/front/settings/Storages.java deleted file mode 100644 index cd09e99..0000000 --- a/src/main/java/com/artipie/front/settings/Storages.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * The MIT License (MIT) Copyright (c) 2022 artipie.com - * https://github.com/artipie/front/LICENSE.txt - */ -package com.artipie.front.settings; - -import java.util.Collection; -import javax.json.JsonObject; - -/** - * Atripie storages settings. - * @since 0.1 - */ -public interface Storages { - - /** - * List artipie storages. - * @return Collection of {@link Storage} instances - */ - Collection list(); - - /** - * Add storage to artipie storages. - * @param alias Storage alias - * @param info Storage settings - */ - void add(String alias, JsonObject info); - - /** - * Remove storage from settings. - * @param alias Storage alias - * @throws com.artipie.front.api.NotFoundException If such storage does not exist - */ - void remove(String alias); - - /** - * Artipie storage. - * @since 0.1 - */ - interface Storage { - - /** - * Storage alias. - * @return Alias - */ - String alias(); - - /** - * Storage settings. - * @return Settings in json format - */ - JsonObject info(); - } -} diff --git a/src/main/java/com/artipie/front/settings/YamlRepoPermissions.java b/src/main/java/com/artipie/front/settings/YamlRepoPermissions.java deleted file mode 100644 index cead0a0..0000000 --- a/src/main/java/com/artipie/front/settings/YamlRepoPermissions.java +++ /dev/null @@ -1,232 +0,0 @@ -/* - * The MIT License (MIT) Copyright (c) 2022 artipie.com - * https://github.com/artipie/front/LICENSE.txt - */ -package com.artipie.front.settings; - -import com.amihaiemil.eoyaml.Yaml; -import com.amihaiemil.eoyaml.YamlMapping; -import com.amihaiemil.eoyaml.YamlMappingBuilder; -import com.amihaiemil.eoyaml.YamlNode; -import com.amihaiemil.eoyaml.YamlSequenceBuilder; -import com.artipie.asto.Key; -import com.artipie.asto.blocking.BlockingStorage; -import com.artipie.front.api.NotFoundException; -import com.artipie.front.misc.Yaml2Json; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.nio.charset.StandardCharsets; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import javax.json.Json; -import javax.json.JsonArray; -import javax.json.JsonObject; -import javax.json.JsonStructure; -import javax.json.JsonValue; - -/** - * Implementation of {@link RepoPermissions} to handle repository permissions. - * This implementation takes into account both .yml and .yaml extensions. - * @since 0.1 - * @checkstyle ExecutableStatementCountCheck (500 lines) - */ -public final class YamlRepoPermissions implements RepoPermissions { - - /** - * Permissions yaml section. - */ - private static final String PERMS = "permissions"; - - /** - * Repo yaml section. - */ - private static final String REPO = "repo"; - - /** - * Artipie repository settings storage. - */ - private final BlockingStorage blsto; - - /** - * Ctor. - * @param blsto Artipie repository settings storage - */ - public YamlRepoPermissions(final BlockingStorage blsto) { - this.blsto = blsto; - } - - @Override - public JsonObject get(final String repo) { - return this.permsYaml(repo).map(YamlMapping::toString).map(new Yaml2Json()) - .map(JsonValue::asJsonObject) - .orElse(Json.createObjectBuilder().build()); - } - - @Override - public void add(final String repo, final String uid, final JsonArray perms) { - final Optional creds = this.permsYaml(repo); - YamlMappingBuilder builder = Yaml.createYamlMappingBuilder(); - if (creds.isPresent()) { - for (final YamlNode node : creds.get().keys()) { - final String usr = node.asScalar().value(); - if (!uid.equals(usr)) { - builder = builder.add(usr, creds.get().yamlSequence(usr)); - } - } - } - YamlSequenceBuilder seq = Yaml.createYamlSequenceBuilder(); - for (final String item : perms.getValuesAs(JsonValue::toString)) { - seq = seq.add(item.replaceAll("\"", "")); - } - builder = builder.add(uid, seq.build()); - this.updateRepoSettings(repo, builder.build()); - } - - @Override - public void delete(final String repo, final String uid) { - final Optional creds = this.permsYaml(repo); - YamlMappingBuilder builder = Yaml.createYamlMappingBuilder(); - if (creds.isPresent() && creds.get().yamlSequence(uid) != null) { - for (final YamlNode node : creds.get().keys()) { - final String usr = node.asScalar().value(); - if (!uid.equals(usr)) { - builder = builder.add(usr, creds.get().yamlSequence(usr)); - } - } - this.updateRepoSettings(repo, builder.build()); - return; - } - throw new NotFoundException( - String.format("User %s does have any permissions in repository %s", uid, repo) - ); - } - - @Override - @SuppressWarnings("PMD.CyclomaticComplexity") - public void patch(final String repo, final JsonStructure perms) { - final Optional creds = this.permsYaml(repo); - YamlMappingBuilder builder = Yaml.createYamlMappingBuilder(); - final JsonObject revoke = perms.asJsonObject().getJsonObject("revoke"); - final JsonObject grant = perms.asJsonObject().getJsonObject("grant"); - final Set granted = new HashSet<>(grant.size()); - if (creds.isPresent()) { - for (final YamlNode node : creds.get().keys()) { - final String usr = node.asScalar().value(); - granted.add(usr); - Stream stream = creds.get().yamlSequence(usr).values() - .stream().map(item -> item.asScalar().value()); - if (revoke.containsKey(usr)) { - stream = stream.filter( - item -> YamlRepoPermissions.toStream(revoke.getJsonArray(usr)) - .noneMatch(rvk -> rvk.equals(item)) - ); - } - if (grant.containsKey(usr)) { - stream = Stream.concat( - stream, YamlRepoPermissions.toStream(grant.getJsonArray(usr)) - ); - } - final List list = stream.collect(Collectors.toList()); - if (!list.isEmpty()) { - YamlSequenceBuilder seq = Yaml.createYamlSequenceBuilder(); - for (final String perm : list) { - seq = seq.add(perm); - } - builder = builder.add(usr, seq.build()); - } - } - } - for (final Map.Entry entry : grant.entrySet()) { - if (!granted.contains(entry.getKey())) { - YamlSequenceBuilder seq = Yaml.createYamlSequenceBuilder(); - final List list = YamlRepoPermissions - .toStream(entry.getValue().asJsonArray()).collect(Collectors.toList()); - for (final String perm : list) { - seq = seq.add(perm); - } - builder = builder.add(entry.getKey(), seq.build()); - } - } - this.updateRepoSettings(repo, builder.build()); - } - - /** - * Updates credentials section in repository settings. - * @param repo Repository name - * @param creds New credentials section - */ - private void updateRepoSettings(final String repo, final YamlMapping creds) { - try { - final YamlMapping stngs = Yaml.createYamlInput( - new String(this.blsto.value(this.key(repo)), StandardCharsets.UTF_8) - ).readYamlMapping().yamlMapping(YamlRepoPermissions.REPO); - YamlMappingBuilder builder = Yaml.createYamlMappingBuilder(); - for (final YamlNode node : stngs.keys()) { - final String map = node.asScalar().value(); - if (!YamlRepoPermissions.PERMS.equals(map)) { - builder = builder.add(map, stngs.value(map)); - } - } - builder = builder.add(YamlRepoPermissions.PERMS, creds); - this.blsto.save( - this.key(repo), - Yaml.createYamlMappingBuilder().add(YamlRepoPermissions.REPO, builder.build()) - .build().toString().getBytes(StandardCharsets.UTF_8) - ); - } catch (final IOException err) { - throw new UncheckedIOException(err); - } - } - - /** - * Get repository settings key. - * @param name Repository name - * @return The key of found - * @throws com.artipie.front.api.NotFoundException If repository does not exist - */ - private Key key(final String name) { - Key res = new Key.From(String.format("%s.yaml", name)); - if (!this.blsto.exists(res)) { - res = new Key.From(String.format("%s.yml", name)); - if (!this.blsto.exists(res)) { - throw new NotFoundException(String.format("Repository %s not found", name)); - } - } - return res; - } - - /** - * Reads repo setting section permissions. - * @param repo Repository name - * @return Credentials section if present - */ - private Optional permsYaml(final String repo) { - try { - return Optional.ofNullable( - Yaml.createYamlInput( - new String(this.blsto.value(this.key(repo)), StandardCharsets.UTF_8) - ).readYamlMapping().yamlMapping(YamlRepoPermissions.REPO) - .yamlMapping(YamlRepoPermissions.PERMS) - ); - } catch (final IOException err) { - throw new UncheckedIOException(err); - } - } - - /** - * Transform json array to stream of strings. - * {@link JsonValue#toString()} method adds extra quotes, so - * we need to remove them. - * @param arr Json array - * @return Stream of strings - */ - private static Stream toStream(final JsonArray arr) { - return arr.getValuesAs(JsonValue::toString) - .stream().map(val -> val.replace("\"", "")); - } -} diff --git a/src/main/java/com/artipie/front/settings/YamlStorage.java b/src/main/java/com/artipie/front/settings/YamlStorage.java deleted file mode 100644 index e3e7726..0000000 --- a/src/main/java/com/artipie/front/settings/YamlStorage.java +++ /dev/null @@ -1,136 +0,0 @@ -/* - * The MIT License (MIT) Copyright (c) 2022 artipie.com - * https://github.com/artipie/front/LICENSE.txt - */ -package com.artipie.front.settings; - -import com.amihaiemil.eoyaml.StrictYamlMapping; -import com.amihaiemil.eoyaml.YamlMapping; -import com.artipie.asto.Storage; -import com.artipie.asto.etcd.EtcdStorage; -import com.artipie.asto.fs.FileStorage; -import com.artipie.asto.s3.S3Storage; -import io.etcd.jetcd.Client; -import io.etcd.jetcd.ClientBuilder; -import java.net.URI; -import java.nio.file.Path; -import java.util.stream.Collectors; -import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; -import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; -import software.amazon.awssdk.regions.Region; -import software.amazon.awssdk.services.s3.S3AsyncClient; -import software.amazon.awssdk.services.s3.S3AsyncClientBuilder; - -/** - * Storage settings built from YAML. - * - * @since 0.1 - */ -@SuppressWarnings("PMD.AvoidDuplicateLiterals") -public final class YamlStorage { - - /** - * YAML storage settings. - */ - private final YamlMapping yaml; - - /** - * Ctor. - * @param yaml YAML storage settings. - */ - public YamlStorage(final YamlMapping yaml) { - this.yaml = yaml; - } - - /** - * Provides a storage. - * - * @return Storage instance. - */ - public Storage storage() { - @SuppressWarnings("deprecation") final YamlMapping strict = - new StrictYamlMapping(this.yaml); - final String type = strict.string("type"); - final Storage storage; - if ("fs".equals(type)) { - storage = new FileStorage(Path.of(strict.string("path"))); - } else if ("s3".equals(type)) { - storage = new S3Storage( - this.s3Client(), - strict.string("bucket"), - !"false".equals(this.yaml.string("multipart")) - ); - } else if ("etcd".equals(type)) { - storage = new EtcdStorage( - YamlStorage.etcdClient(strict.yamlMapping("connection")) - ); - } else { - throw new IllegalStateException(String.format("Unsupported storage type: '%s'", type)); - } - return storage; - } - - /** - * Creates {@link S3AsyncClient} instance based on YAML config. - * - * @return Built S3 client. - * @checkstyle MethodNameCheck (3 lines) - */ - @SuppressWarnings("deprecation") - private S3AsyncClient s3Client() { - final S3AsyncClientBuilder builder = S3AsyncClient.builder(); - final String region = this.yaml.string("region"); - if (region != null) { - builder.region(Region.of(region)); - } - final String endpoint = this.yaml.string("endpoint"); - if (endpoint != null) { - builder.endpointOverride(URI.create(endpoint)); - } - return builder - .credentialsProvider( - credentials(new StrictYamlMapping(this.yaml).yamlMapping("credentials")) - ) - .build(); - } - - /** - * Build etcd client from yaml config. - * @param yaml Etcd config - * @return Etcd client - */ - private static Client etcdClient(final YamlMapping yaml) { - final ClientBuilder builder = Client.builder().endpoints( - yaml.yamlSequence("endpoints") - .values().stream().map(node -> node.asScalar().value()) - .collect(Collectors.toList()).toArray(new String[0]) - ); - final String sto = yaml.string("timeout"); - if (sto != null) { - builder.connectTimeoutMs(Integer.valueOf(sto)); - } - return builder.build(); - } - - /** - * Creates {@link StaticCredentialsProvider} instance based on YAML config. - * - * @param yaml Credentials config YAML. - * @return Credentials provider. - */ - private static StaticCredentialsProvider credentials(final YamlMapping yaml) { - final String type = yaml.string("type"); - if ("basic".equals(type)) { - return StaticCredentialsProvider.create( - AwsBasicCredentials.create( - yaml.string("accessKeyId"), - yaml.string("secretAccessKey") - ) - ); - } else { - throw new IllegalArgumentException( - String.format("Unsupported S3 credentials type: %s", type) - ); - } - } -} diff --git a/src/main/java/com/artipie/front/settings/YamlStorages.java b/src/main/java/com/artipie/front/settings/YamlStorages.java deleted file mode 100644 index 8b140b5..0000000 --- a/src/main/java/com/artipie/front/settings/YamlStorages.java +++ /dev/null @@ -1,216 +0,0 @@ -/* - * The MIT License (MIT) Copyright (c) 2022 artipie.com - * https://github.com/artipie/front/LICENSE.txt - */ -package com.artipie.front.settings; - -import com.amihaiemil.eoyaml.Yaml; -import com.amihaiemil.eoyaml.YamlMapping; -import com.amihaiemil.eoyaml.YamlMappingBuilder; -import com.amihaiemil.eoyaml.YamlNode; -import com.artipie.asto.Key; -import com.artipie.asto.blocking.BlockingStorage; -import com.artipie.front.api.NotFoundException; -import com.artipie.front.misc.Json2Yaml; -import com.artipie.front.misc.Yaml2Json; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.nio.charset.StandardCharsets; -import java.util.Collection; -import java.util.Collections; -import java.util.Optional; -import java.util.stream.Collectors; -import javax.json.JsonObject; - -/** - * Implementation of {@link Storages} to manage storages yaml file settings. This implementation - * takes info account .yaml/.yml extensions. Note, that storages settings file can be present as - * for the whole artipie as for repository individually. In the case of `org` layout, storages - * can be set for user or for user repository. - * @since 0.1 - */ -public final class YamlStorages implements Storages { - - /** - * Settings file name. - */ - public static final String FILE_NAME = "_storages"; - - /** - * Key for the settings file with .yaml extension. - */ - private static final Key YAML = new Key.From(String.format("%s.yaml", YamlStorages.FILE_NAME)); - - /** - * Yaml storages section name. - */ - private static final String STORAGES_NODE = "storages"; - - /** - * Repository or user key. - */ - private final Optional key; - - /** - * Storage. - */ - private final BlockingStorage blsto; - - /** - * Ctor. - * @param key Repository or user key - * @param blsto Storage - */ - public YamlStorages(final Optional key, final BlockingStorage blsto) { - this.key = key; - this.blsto = blsto; - } - - /** - * Ctor. - * @param key Repository or user key - * @param blsto Storage - */ - public YamlStorages(final Key key, final BlockingStorage blsto) { - this(Optional.of(key), blsto); - } - - /** - * Ctor. - * @param blsto Storage - */ - public YamlStorages(final BlockingStorage blsto) { - this(Optional.empty(), blsto); - } - - @Override - public Collection list() { - final Optional storages = this.storages(); - return storages.map( - nodes -> nodes.keys().stream().map(node -> node.asScalar().value()).map( - alias -> new YamlStorage(alias, storages.get().yamlMapping(alias)) - ).collect(Collectors.toList()) - ).orElse(Collections.emptyList()); - } - - @Override - public void add(final String alias, final JsonObject info) { - YamlMappingBuilder builder = Yaml.createYamlMappingBuilder(); - final Optional storages = this.storages(); - if (storages.isPresent()) { - for (final YamlNode node : storages.get().keys()) { - final String name = node.asScalar().value(); - builder = builder.add(name, storages.get().yamlMapping(name)); - } - } - builder = builder.add(alias, new Json2Yaml().apply(info.toString())); - this.blsto.save( - this.settingKey().orElse( - this.key.map(val -> new Key.From(val, YamlStorages.YAML)) - .orElse(YamlStorages.YAML) - ), - Yaml.createYamlMappingBuilder().add(YamlStorages.STORAGES_NODE, builder.build()) - .build().toString().getBytes(StandardCharsets.UTF_8) - ); - } - - @Override - public void remove(final String alias) { - final Optional storages = this.storages(); - if (storages.isPresent() && storages.get().value(alias) != null) { - YamlMappingBuilder builder = Yaml.createYamlMappingBuilder(); - for (final YamlNode node : storages.get().keys()) { - final String name = node.asScalar().value(); - if (!alias.equals(name)) { - builder = builder.add(name, storages.get().yamlMapping(name)); - } - } - this.blsto.save( - this.settingKey().get(), - Yaml.createYamlMappingBuilder().add(YamlStorages.STORAGES_NODE, builder.build()) - .build().toString().getBytes(StandardCharsets.UTF_8) - ); - return; - } - throw new NotFoundException(String.format("Storage alias %s does not exist", alias)); - } - - /** - * Returns storages yaml mapping if found. - * @return Settings storages yaml - */ - private Optional storages() { - final Optional stng = this.settingKey(); - Optional res = Optional.empty(); - if (stng.isPresent()) { - try { - res = Optional.ofNullable( - Yaml.createYamlInput( - new String(this.blsto.value(stng.get()), StandardCharsets.UTF_8) - ).readYamlMapping().yamlMapping(YamlStorages.STORAGES_NODE) - ); - } catch (final IOException err) { - throw new UncheckedIOException(err); - } - } - return res; - } - - /** - * Finds storages settings key. - * @return The key if found - */ - private Optional settingKey() { - Optional res = Optional.of( - this.key.map(val -> new Key.From(val, YamlStorages.YAML)) - .orElse(YamlStorages.YAML) - ); - if (!this.blsto.exists(res.get())) { - final String yml = String.format("%s.yml", YamlStorages.FILE_NAME); - res = Optional.of( - this.key.map(val -> new Key.From(val, yml)).orElse(new Key.From(yml)) - ); - if (!this.blsto.exists(res.get())) { - res = Optional.empty(); - } - } - return res; - } - - /** - * Implementation of {@link com.artipie.front.settings.Storages.Storage} from Yaml. - * @since 0.1 - */ - static final class YamlStorage implements Storage { - - /** - * Storage alias name. - */ - private final String name; - - /** - * Storage yaml mapping. - */ - private final YamlMapping yaml; - - /** - * Ctor. - * @param name Storage alias name - * @param yaml Storage yaml mapping - */ - YamlStorage(final String name, final YamlMapping yaml) { - this.name = name; - this.yaml = yaml; - } - - @Override - public String alias() { - return this.name; - } - - @Override - public JsonObject info() { - return new Yaml2Json().apply(this.yaml.toString()).asJsonObject(); - } - } -} diff --git a/src/main/java/com/artipie/front/ui/HbPage.java b/src/main/java/com/artipie/front/ui/HbPage.java index e887527..55d6e8a 100644 --- a/src/main/java/com/artipie/front/ui/HbPage.java +++ b/src/main/java/com/artipie/front/ui/HbPage.java @@ -15,7 +15,7 @@ * Handlebars page. * @since 1.0 */ -final class HbPage implements TemplateViewRoute { +public final class HbPage implements TemplateViewRoute { /** * Template name. @@ -32,7 +32,7 @@ final class HbPage implements TemplateViewRoute { * @param template Template name * @param params Params */ - HbPage(final String template, final Map params) { + public HbPage(final String template, final Map params) { this(template, req -> params); } @@ -41,7 +41,7 @@ final class HbPage implements TemplateViewRoute { * @param template Template name * @param params Create params from request */ - HbPage(final String template, + public HbPage(final String template, final Function> params) { this.template = template; this.params = params; diff --git a/src/main/java/com/artipie/front/ui/PostSignIn.java b/src/main/java/com/artipie/front/ui/PostSignIn.java index 0defd22..1c97e0a 100644 --- a/src/main/java/com/artipie/front/ui/PostSignIn.java +++ b/src/main/java/com/artipie/front/ui/PostSignIn.java @@ -4,7 +4,7 @@ */ package com.artipie.front.ui; -import com.artipie.front.auth.AuthByPassword; +import com.artipie.front.rest.AuthService; import java.util.Objects; import org.eclipse.jetty.http.HttpStatus; import spark.Request; @@ -14,22 +14,22 @@ /** * Signin form POST handler. - * @since 1.0 + * * @checkstyle AvoidDuplicateLiterals (500 lines) + * @since 1.0 */ @SuppressWarnings("PMD.AvoidDuplicateLiterals") public final class PostSignIn implements Route { - /** - * Password authenticator. + * Auth service. */ - private final AuthByPassword auth; + private final AuthService auth; /** * New signin form processor. - * @param auth Password auth + * @param auth Auth service. */ - public PostSignIn(final AuthByPassword auth) { + public PostSignIn(final AuthService auth) { this.auth = auth; } @@ -38,6 +38,16 @@ public Object handle(final Request req, final Response rsp) throws Exception { if (req.session() == null) { Spark.halt(HttpStatus.BAD_REQUEST_400, "session is empty"); } + PostSignIn.checkCrsf(req); + this.receiveToken(req, rsp); + return "Ok"; + } + + /** + * Check crsf. + * @param req Request. + */ + private static void checkCrsf(final Request req) { final String crsf = req.session().attribute("crsf"); req.session().removeAttribute("crsf"); final var valid = Objects.equals( @@ -46,17 +56,20 @@ public Object handle(final Request req, final Response rsp) throws Exception { if (!valid) { Spark.halt(HttpStatus.BAD_REQUEST_400, "CRSF validation failed"); } - final var uid = this.auth.authenticate( + } + + /** + * Receives JWT-token. + * @param req Request. + * @param rsp Response. + */ + private void receiveToken(final Request req, final Response rsp) { + final String token = this.auth.getJwtToken( req.queryParamOrDefault("username", ""), req.queryParamOrDefault("password", "") ); - uid.ifPresentOrElse( - val -> { - req.session().attribute("uid", val); - rsp.redirect("/dashboard"); - }, - () -> Spark.halt(HttpStatus.UNAUTHORIZED_401, "bad credentials") - ); - return "OK"; + req.session().attribute("token", token); + req.session().attribute("uid", req.queryParamOrDefault("username", "")); + rsp.redirect("/dashboard"); } } diff --git a/src/main/java/com/artipie/front/ui/RepoPage.java b/src/main/java/com/artipie/front/ui/RepoPage.java deleted file mode 100644 index be27944..0000000 --- a/src/main/java/com/artipie/front/ui/RepoPage.java +++ /dev/null @@ -1,166 +0,0 @@ -/* - * The MIT License (MIT) Copyright (c) 2022 artipie.com - * https://github.com/artipie/front/LICENSE.txt - */ -package com.artipie.front.ui; - -import com.amihaiemil.eoyaml.Yaml; -import com.amihaiemil.eoyaml.YamlMapping; -import com.artipie.front.RequestAttr; -import com.artipie.front.api.Repositories; -import com.artipie.front.api.Users; -import com.artipie.front.misc.RouteWrap; -import com.artipie.front.misc.ValueFromBody; -import com.artipie.front.settings.RepoSettings; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.nio.charset.StandardCharsets; -import java.util.HashMap; -import java.util.Map; -import org.eclipse.jetty.http.HttpStatus; -import spark.Request; -import spark.Response; -import spark.Route; - -/** - * Dashboard repo page. - * @since 0.1 - */ -@SuppressWarnings("PMD.AvoidDuplicateLiterals") -public final class RepoPage { - - /** - * Dashboard repo page template view. - * @since 0.1 - */ - public static final class TemplateView extends RouteWrap.TemplateViewRoute { - - /** - * Template view wrap route. - * - * @param repos Repository settings - */ - public TemplateView(final RepoSettings repos) { - super( - new HbPage( - "repo", - req -> { - final String uid = RequestAttr.Standard.USER_ID.readOrThrow(req); - final String name = Repositories.REPO_PARAM.parse(req); - final Map res = new HashMap<>(5); - res.put("user", uid); - res.put("title", uid); - res.put("name", name); - if (repos.exists(name, uid)) { - try { - final YamlMapping yaml = Yaml.createYamlInput( - new ByteArrayInputStream(repos.value(name, uid)) - ).readYamlMapping(); - res.put("type", yaml.yamlMapping("repo").string("type")); - res.put("config", yaml.toString()); - } catch (final IOException err) { - throw new UncheckedIOException(err); - } - } else { - res.put("type", req.queryParamOrDefault("type", "maven")); - } - return res; - } - ) - ); - } - } - - /** - * Handles post request from dashboard repo page to remove/update repository. - * @since 0.1 - * @checkstyle ExecutableStatementCountCheck (100 lines) - * @checkstyle ReturnCountCheck (100 lines) - */ - public static final class Post implements Route { - - /** - * Repository settings. - */ - private final RepoSettings stn; - - /** - * Ctor. - * @param stn Repository settings - */ - public Post(final RepoSettings stn) { - this.stn = stn; - } - - @Override - @SuppressWarnings("PMD.OnlyOneReturn") - public Object handle(final Request request, final Response response) { - final ValueFromBody vals = new ValueFromBody(request.body()); - final String action = vals.byNameOrThrow("action"); - final String name = vals.byNameOrThrow("repo"); - final String uid = Users.USER_PARAM.parse(request); - if ("update".equals(action)) { - final YamlMapping yaml = Post.configsFromBody(vals); - final YamlMapping repo = yaml.yamlMapping("repo"); - if (repo == null) { - response.status(HttpStatus.BAD_REQUEST_400); - return "Repo section is required"; - } - if (repo.value("type") == null) { - response.status(HttpStatus.BAD_REQUEST_400); - return "Repository type is required"; - } - if (repo.value("storage") == null) { - response.status(HttpStatus.BAD_REQUEST_400); - return "Repository storage is required"; - } - if (this.stn.exists(name, uid)) { - this.stn.delete(name, uid); - } - this.stn.save(name, uid, yaml.toString().getBytes(StandardCharsets.UTF_8)); - response.redirect(String.format("/dashboard/%s/%s", uid, name)); - } else if ("delete".equals(action)) { - this.stn.delete(name, uid); - response.redirect("/dashboard"); - } - response.status(HttpStatus.FOUND_302); - return null; - } - - /** - * Obtains config from body. - * @param vals Values in body - * @return Config content from body - */ - private static YamlMapping configsFromBody(final ValueFromBody vals) { - try { - return Yaml.createYamlInput(vals.byNameOrThrow("config")).readYamlMapping(); - } catch (final IOException err) { - throw new UncheckedIOException(err); - } - } - } - - /** - * Handle get request while creating new repository. - * @since 0.1 - */ - public static final class Get implements Route { - - @Override - public Object handle(final Request request, final Response response) { - response.status(HttpStatus.FOUND_302); - response.redirect( - String.format( - "/dashboard/%s/%s?type=%s", - RequestAttr.Standard.USER_ID.readOrThrow(request), - request.queryParams("repo"), - request.queryParams("type") - ) - ); - return null; - } - } - -} diff --git a/src/main/java/com/artipie/front/ui/UserPage.java b/src/main/java/com/artipie/front/ui/UserPage.java deleted file mode 100644 index 2efe251..0000000 --- a/src/main/java/com/artipie/front/ui/UserPage.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * The MIT License (MIT) Copyright (c) 2022 artipie.com - * https://github.com/artipie/front/LICENSE.txt - */ -package com.artipie.front.ui; - -import com.artipie.front.RequestAttr; -import com.artipie.front.misc.RouteWrap; -import com.artipie.front.settings.RepoSettings; -import java.util.Map; -import java.util.Optional; - -/** - * Dashboard user page. - * @since 0.1 - */ -@SuppressWarnings("PMD.AvoidDuplicateLiterals") -public final class UserPage extends RouteWrap.TemplateViewRoute { - - /** - * New user page. - * @param repos Artipie repository settings - */ - public UserPage(final RepoSettings repos) { - super( - new HbPage( - "user", - req -> { - final String uid = RequestAttr.Standard.USER_ID.readOrThrow(req); - return Map.of( - "user", uid, "title", uid, - "repos", repos.list(Optional.of(uid)) - ); - } - ) - ); - } -} diff --git a/src/main/java/com/artipie/front/ui/repository/RepoAddConfig.java b/src/main/java/com/artipie/front/ui/repository/RepoAddConfig.java new file mode 100644 index 0000000..43d7ae0 --- /dev/null +++ b/src/main/java/com/artipie/front/ui/repository/RepoAddConfig.java @@ -0,0 +1,65 @@ +/* + * The MIT License (MIT) Copyright (c) 2022 artipie.com + * https://github.com/artipie/front/LICENSE.txt + */ +package com.artipie.front.ui.repository; + +import com.artipie.front.Layout; +import com.artipie.front.misc.RouteWrap; +import com.artipie.front.rest.RepositoryName; +import com.artipie.front.ui.HbPage; +import java.util.Map; + +/** + * Add repository config view page. + * + * @since 1.0 + */ +@SuppressWarnings("PMD.AvoidDuplicateLiterals") +public final class RepoAddConfig extends RouteWrap.TemplateViewRoute { + /** + * Add repository info page. + * @param layout Layout + * @param info Infor template + * @param template Repository template + */ + public RepoAddConfig(final Layout layout, final RepositoryInfo info, + final RepositoryTemplate template) { + super( + new HbPage( + "repository/add_config", + req -> { + final String name = req.queryParams("name"); + final String type = req.queryParams("type"); + final String uid = req.session().attribute("uid"); + final String rname; + if (layout == Layout.FLAT) { + rname = new RepositoryName.Flat(name).toString(); + } else { + rname = new RepositoryName.Org(name, uid).toString(); + } + return Map.of( + "title", "Add repository", + "rname", rname, + "info", info.render( + type, + Map.of( + "user", uid, + "repo", name, + "type", type + ) + ), + "template", template.render( + type, + Map.of( + "user", uid, + "repo", name, + "type", type + ) + ) + ); + } + ) + ); + } +} diff --git a/src/main/java/com/artipie/front/ui/repository/RepoAddInfo.java b/src/main/java/com/artipie/front/ui/repository/RepoAddInfo.java new file mode 100644 index 0000000..ed9661f --- /dev/null +++ b/src/main/java/com/artipie/front/ui/repository/RepoAddInfo.java @@ -0,0 +1,31 @@ +/* + * The MIT License (MIT) Copyright (c) 2022 artipie.com + * https://github.com/artipie/front/LICENSE.txt + */ +package com.artipie.front.ui.repository; + +import com.artipie.front.misc.RouteWrap; +import com.artipie.front.ui.HbPage; +import java.util.Map; + +/** + * Add repository info view page. + * + * @since 1.0 + */ +@SuppressWarnings("PMD.AvoidDuplicateLiterals") +public final class RepoAddInfo extends RouteWrap.TemplateViewRoute { + /** + * Add repository info page. + */ + public RepoAddInfo() { + super( + new HbPage( + "repository/add_info", + req -> Map.of( + "title", "Add repository" + ) + ) + ); + } +} diff --git a/src/main/java/com/artipie/front/ui/repository/RepoEdit.java b/src/main/java/com/artipie/front/ui/repository/RepoEdit.java new file mode 100644 index 0000000..aa3f4a3 --- /dev/null +++ b/src/main/java/com/artipie/front/ui/repository/RepoEdit.java @@ -0,0 +1,95 @@ +/* + * The MIT License (MIT) Copyright (c) 2022 artipie.com + * https://github.com/artipie/front/LICENSE.txt + */ +package com.artipie.front.ui.repository; + +import com.amihaiemil.eoyaml.Yaml; +import com.amihaiemil.eoyaml.YamlMapping; +import com.artipie.front.Layout; +import com.artipie.front.misc.RouteWrap; +import com.artipie.front.rest.RepositoryName; +import com.artipie.front.rest.RepositoryService; +import com.artipie.front.ui.HbPage; +import java.io.IOException; +import java.util.Map; +import java.util.Optional; +import org.eclipse.jetty.io.RuntimeIOException; + +/** + * Repository editor page. + * + * @since 1.0 + */ +@SuppressWarnings("PMD.AvoidDuplicateLiterals") +public final class RepoEdit extends RouteWrap.TemplateViewRoute { + /** + * Repository editor page. + * + * @param repository Repository service. + * @param layout Layout. + * @param info Repository info templates. + */ + public RepoEdit(final RepositoryService repository, final Layout layout, + final RepositoryInfo info) { + super( + new HbPage( + "repository/edit", + req -> { + final RepositoryName rname = new RepositoryName.FromRequest(req, layout); + final String repo = req.params(":repo"); + final String uid = req.session().attribute("uid"); + final String conf = repository.repo( + req.session().attribute("token"), rname + ); + return Map.of( + "title", String.format("Repository %s", rname), + "rname", rname, + "conf", conf, + "info", RepoEdit.repoType(conf) + .map( + type -> { + String rendered; + try { + rendered = info.render( + type, + Map.of( + "user", uid, + "repo", repo, + "type", type + ) + ); + } catch (final RuntimeIOException exc) { + rendered = ""; + } + return rendered; + } + ).orElse("") + ); + } + ) + ); + } + + /** + * Provides repository type from yaml-configuration. + * @param repoconf Yaml-content of repository configuration. + * @return Repository type. + */ + private static Optional repoType(final String repoconf) { + return Optional.ofNullable(repoconf) + .flatMap( + content -> { + Optional yaml; + try { + yaml = Optional.of(Yaml.createYamlInput(content).readYamlMapping()); + } catch (final IOException exc) { + yaml = Optional.empty(); + } + return yaml; + } + ) + .map(yaml -> yaml.yamlMapping("repo")) + .map(repo -> repo.string("type")); + } +} diff --git a/src/main/java/com/artipie/front/ui/repository/RepoList.java b/src/main/java/com/artipie/front/ui/repository/RepoList.java new file mode 100644 index 0000000..695422f --- /dev/null +++ b/src/main/java/com/artipie/front/ui/repository/RepoList.java @@ -0,0 +1,49 @@ +/* + * The MIT License (MIT) Copyright (c) 2022 artipie.com + * https://github.com/artipie/front/LICENSE.txt + */ +package com.artipie.front.ui.repository; + +import com.artipie.front.Layout; +import com.artipie.front.misc.RouteWrap; +import com.artipie.front.rest.RepositoryService; +import com.artipie.front.ui.HbPage; +import java.util.List; +import java.util.Map; + +/** + * List of repositories page. + * + * @checkstyle AvoidDuplicateLiterals (500 lines) + * @since 1.0 + */ +@SuppressWarnings("PMD.AvoidDuplicateLiterals") +public final class RepoList extends RouteWrap.TemplateViewRoute { + /** + * List of repositories page. + * + * @param repository Repository service. + * @param layout Layout. + */ + public RepoList(final RepositoryService repository, final Layout layout) { + super( + new HbPage( + "repository/list", + req -> { + final String uid = req.session().attribute("uid"); + final String token = req.session().attribute("token"); + final List repos; + if ("flat".equals(layout)) { + repos = repository.list(token); + } else { + repos = repository.list(token, uid); + } + return Map.of( + "title", "Repository list", + "repos", repos + ); + } + ) + ); + } +} diff --git a/src/main/java/com/artipie/front/ui/repository/RepoRemove.java b/src/main/java/com/artipie/front/ui/repository/RepoRemove.java new file mode 100644 index 0000000..cfb83d2 --- /dev/null +++ b/src/main/java/com/artipie/front/ui/repository/RepoRemove.java @@ -0,0 +1,45 @@ +/* + * The MIT License (MIT) Copyright (c) 2022 artipie.com + * https://github.com/artipie/front/LICENSE.txt + */ +package com.artipie.front.ui.repository; + +import com.artipie.front.Layout; +import com.artipie.front.misc.RouteWrap; +import com.artipie.front.rest.RepositoryName; +import com.artipie.front.rest.RepositoryService; +import com.artipie.front.ui.HbPage; +import java.util.Map; + +/** + * Repository remove POST-handler. + * Removes repository and shows result of on page. + * + * @since 1.0 + */ +@SuppressWarnings("PMD.AvoidDuplicateLiterals") +public final class RepoRemove extends RouteWrap.TemplateViewRoute { + /** + * Repository delete. + * + * @param repository Repository service. + * @param layout Layout. + */ + public RepoRemove(final RepositoryService repository, final Layout layout) { + super( + new HbPage( + "repository/result", + req -> { + final RepositoryName rname = new RepositoryName.FromRequest(req, layout); + return Map.of( + "title", String.format("Repository %s", rname), + "result", repository.remove(req.session().attribute("token"), rname), + "redirectUrl", "/dashboard/repository/list", + "redirectMessage", "Continue" + ); + } + ) + ); + } +} + diff --git a/src/main/java/com/artipie/front/ui/repository/RepoSave.java b/src/main/java/com/artipie/front/ui/repository/RepoSave.java new file mode 100644 index 0000000..9ed3121 --- /dev/null +++ b/src/main/java/com/artipie/front/ui/repository/RepoSave.java @@ -0,0 +1,49 @@ +/* + * The MIT License (MIT) Copyright (c) 2022 artipie.com + * https://github.com/artipie/front/LICENSE.txt + */ +package com.artipie.front.ui.repository; + +import com.artipie.front.Layout; +import com.artipie.front.misc.RouteWrap; +import com.artipie.front.rest.RepositoryName; +import com.artipie.front.rest.RepositoryService; +import com.artipie.front.ui.HbPage; +import java.util.Map; + +/** + * Repository configuration saver. + * Saves repository configuration and shows result of saving on page. + * + * @since 1.0 + */ +@SuppressWarnings("PMD.AvoidDuplicateLiterals") +public final class RepoSave extends RouteWrap.TemplateViewRoute { + /** + * List of repositories page. + * + * @param repository Repository service. + * @param layout Layout. + */ + public RepoSave(final RepositoryService repository, final Layout layout) { + super( + new HbPage( + "repository/result", + req -> { + final RepositoryName rname = new RepositoryName.FromRequest(req, layout); + return Map.of( + "title", String.format("Repository %s", rname), + "result", repository.save( + req.session().attribute("token"), + rname, + req.queryParams("config") + ), + "redirectUrl", String.format("/dashboard/repository/edit/%s", rname), + "redirectMessage", "Continue" + ); + } + ) + ); + } +} + diff --git a/src/main/java/com/artipie/front/ui/repository/RepositoryInfo.java b/src/main/java/com/artipie/front/ui/repository/RepositoryInfo.java new file mode 100644 index 0000000..62a6252 --- /dev/null +++ b/src/main/java/com/artipie/front/ui/repository/RepositoryInfo.java @@ -0,0 +1,68 @@ +/* + * The MIT License (MIT) Copyright (c) 2022 artipie.com + * https://github.com/artipie/front/LICENSE.txt + */ +package com.artipie.front.ui.repository; + +import com.artipie.front.ui.HbTemplateEngine; +import java.util.Map; +import spark.ModelAndView; + +/** + * Info-template renderer. + * Provides 'html'-template with information about repository usage of specified type. + * All info-templates are situated in '/info'-resource folder. + * + * @since 1.0 + */ +public final class RepositoryInfo { + /** + * Format for info-template. + */ + private static final String FORMAT = "%s.info.html"; + + /** + * Type-aliases. + */ + private static final Map ALIAS = Map.of("binary", "file"); + + /** + * Template engine. + */ + private final HbTemplateEngine template; + + /** + * Ctor. + */ + public RepositoryInfo() { + this.template = new HbTemplateEngine("/info"); + } + + /** + * Renders template. + * @param type Repository type. + * @param model Model. + * @return Rendered template + */ + public String render(final String type, final Map model) { + return this.template.render( + new ModelAndView( + model, + String.format(RepositoryInfo.FORMAT, RepositoryInfo.resolveAlias(type)) + ) + ); + } + + /** + * Resolve alias-type to type. + * @param alias Alias-type. + * @return Resolved type. + */ + private static String resolveAlias(final String alias) { + String type = alias; + if (RepositoryInfo.ALIAS.containsKey(alias)) { + type = RepositoryInfo.ALIAS.get(alias); + } + return type; + } +} diff --git a/src/main/java/com/artipie/front/ui/repository/RepositoryTemplate.java b/src/main/java/com/artipie/front/ui/repository/RepositoryTemplate.java new file mode 100644 index 0000000..f613ffd --- /dev/null +++ b/src/main/java/com/artipie/front/ui/repository/RepositoryTemplate.java @@ -0,0 +1,74 @@ +/* + * The MIT License (MIT) Copyright (c) 2022 artipie.com + * https://github.com/artipie/front/LICENSE.txt + */ +package com.artipie.front.ui.repository; + +import com.artipie.front.ui.HbTemplateEngine; +import java.util.List; +import java.util.Map; +import spark.ModelAndView; + +/** + * Repository template renderer. + * Provides 'yaml'-template of repository configuration. + * Provides 'yaml'-template with repository configuration of specified type. + * All templates are situated in '/template'-resource folder. + * + * @since 1.0 + */ +public final class RepositoryTemplate { + /** + * Default template. + */ + private static final String DEFAULT_TEMPLATE = "default"; + + /** + * Format for template. + */ + private static final String FORMAT = "%s.template.yaml"; + + /** + * Pre-defined template names. + */ + private static final List PREDEFINED = List.of("maven-group", "maven-proxy"); + + /** + * Template engine. + */ + private final HbTemplateEngine template; + + /** + * Ctor. + */ + public RepositoryTemplate() { + this.template = new HbTemplateEngine("/template"); + } + + /** + * Render template. + * + * @param type Repository type. + * @param model Model. + * @return Rendered yaml-template for repository type. + */ + public String render(final String type, final Map model) { + final String content; + if (RepositoryTemplate.PREDEFINED.contains(type)) { + content = this.template.render( + new ModelAndView( + model, + String.format(RepositoryTemplate.FORMAT, type) + ) + ); + } else { + content = this.template.render( + new ModelAndView( + model, + String.format(RepositoryTemplate.FORMAT, RepositoryTemplate.DEFAULT_TEMPLATE) + ) + ); + } + return content; + } +} diff --git a/src/main/java/com/artipie/front/api/package-info.java b/src/main/java/com/artipie/front/ui/repository/package-info.java similarity index 65% rename from src/main/java/com/artipie/front/api/package-info.java rename to src/main/java/com/artipie/front/ui/repository/package-info.java index 65c3e5d..e55d622 100644 --- a/src/main/java/com/artipie/front/api/package-info.java +++ b/src/main/java/com/artipie/front/ui/repository/package-info.java @@ -4,7 +4,7 @@ */ /** - * Public API. + * Artipie front web service. * @since 1.0 */ -package com.artipie.front.api; +package com.artipie.front.ui.repository; diff --git a/src/main/resources/html/base b/src/main/resources/html/base index 81442d9..1c5e165 100644 --- a/src/main/resources/html/base +++ b/src/main/resources/html/base @@ -18,12 +18,33 @@ -
-
- {{#block "content"}} - {{/block}} -
-
+ + + + + +
+ + +
+
+ {{#block "content"}} + {{/block}} +
+
+