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-cli1.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.corejackson-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 extends com.artipie.front.settings.Storages.Storage> 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 extends com.artipie.front.settings.Storages.Storage> 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 extends User> 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 extends String> 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 extends String> 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 extends String> 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 extends User> 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 extends String> 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 extends User> 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 extends Storage> 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 extends Storage> 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 super Request, ? extends Map> 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}}
-
-
+
Tag your image with central.artipie.com/{{user}}/{{repo}} image prefix,
-and push it to central.artipie.com then. E.g.
-for alpine:3.11 use:
-
-docker tag alpine:3.11 central.artipie.com/{{user}}/{{repo}}alpine:3.11
-docker push central.artipie.com/{{user}}/{{repo}}alpine:3.11
-
-
-{{/eq}}
-{{#eq type "maven-group"}}
-
To use Maven grouped (virtual) repository first create at least two Maven or Maven proxy repositories.
-It can be either just a Maven repository or Maven proxy (mirror) repository. Then specify
-https://central.artipie.com/{{user}}/{{repo}} as distribution management, or
-repositories in pom.xml or as a mirror in settings.xml (see
-more details in concrete repositories documentation). Then group repositories under
-group-settings, pay attention that order does matter: first repository in the list will be
-accessed first, then second, etc. Repository name should be fully formatted, include your username
-prefix, e.g. {{user}}/maven.
-{{/eq}}{{#eq type "file"}}
-
It's just a binary files repository. Use PUT HTTP request to upload a file,
-and GET for downloading.
-Now you can install packages directly from Artipie:
-
-go get -x -insecure golang.org/x/time
-
-{{/eq}}{{#eq type "rpm"}}
-To install packages from rpm Artipie repository, add the following file
-to yum settings by path /etc/yum.repos.d/example.repo:
-
-Then list and install packages with specified repository:
-
-yum -y repo-pkgs example list
-yum -y repo-pkgs example install
-
-{{/eq}}{{#eq type "conda"}}
-To install conda packages from Artipie, add repository to conda channels settings to /root/.condarc file (check
-documentation for more details):
-
-You can also set automatic upload after building package:
-
-conda config --set anaconda_upload yes
-
-
-Now you can install packages from Artipie anaconda repository using conda install
-command and build and upload packages with conda build, or, if the package is already build,
-use anaconda upload command to publish package to Artipie.
-{{/eq}}{{#eq type "npm"}}
-To install or publish npm package into Artipie repository, specify the repository url
-with the --registry option:
-
+ DISCLAIMER:
+ The service provided to you by Artipie is free of charge
+ and we expect you to behave good. Don't host here anything aside from
+ your private software packages. Too large or abusive files may be deleted without
+ notice. We truly hope that you enjoy our service and want it to stay alive.
+ Thanks!
+
+{{#if redirectUrl}}
+{{redirectMessage}}
+{{/if}}
+
+{{/partial}}
+{{> base}}
\ No newline at end of file
diff --git a/src/main/resources/html/restError b/src/main/resources/html/restError
new file mode 100644
index 0000000..00ecc3d
--- /dev/null
+++ b/src/main/resources/html/restError
@@ -0,0 +1,5 @@
+{{#partial "content"}}
+
Error: {{errorMessage}}
+
Status cde: {{statusCode}}
+{{/partial}}
+{{> base}}
\ No newline at end of file
diff --git a/src/main/resources/info/conda.info.html b/src/main/resources/info/conda.info.html
new file mode 100644
index 0000000..f55006a
--- /dev/null
+++ b/src/main/resources/info/conda.info.html
@@ -0,0 +1,18 @@
+To install conda packages from Artipie, add repository to conda channels settings to /root/.condarc file (check
+documentation for more details):
+
+You can also set automatic upload after building package:
+
+conda config --set anaconda_upload yes
+
+
+Now you can install packages from Artipie anaconda repository using conda install
+command and build and upload packages with conda build, or, if the package is already build,
+use anaconda upload command to publish package to Artipie.
\ No newline at end of file
diff --git a/src/main/resources/info/deb.info.html b/src/main/resources/info/deb.info.html
new file mode 100644
index 0000000..a9aa952
--- /dev/null
+++ b/src/main/resources/info/deb.info.html
@@ -0,0 +1,6 @@
+Add local repository to the list of Debian packages for apt by adding
+the following line to the /etc/apt/sources.list:
+
+deb [trusted=yes] http://{{user}}:password@central.artipie.com/{{user}}/{{repo}} {{repo}} main
+
+Then use apt-get as usual.
\ No newline at end of file
diff --git a/src/main/resources/info/docker.info.html b/src/main/resources/info/docker.info.html
new file mode 100644
index 0000000..ea85011
--- /dev/null
+++ b/src/main/resources/info/docker.info.html
@@ -0,0 +1,8 @@
+
Tag your image with central.artipie.com/{{user}}/{{repo}} image prefix,
+and push it to central.artipie.com then. E.g.
+for alpine:3.11 use:
+
+docker tag alpine:3.11 central.artipie.com/{{user}}/{{repo}}alpine:3.11
+docker push central.artipie.com/{{user}}/{{repo}}alpine:3.11
+
+
\ No newline at end of file
diff --git a/src/main/resources/info/file-proxy.info.html b/src/main/resources/info/file-proxy.info.html
new file mode 100644
index 0000000..e69de29
diff --git a/src/main/resources/info/file.info.html b/src/main/resources/info/file.info.html
new file mode 100644
index 0000000..3bf5f0c
--- /dev/null
+++ b/src/main/resources/info/file.info.html
@@ -0,0 +1,9 @@
+
It's just a binary files repository. Use PUT HTTP request to upload a file,
+and GET for downloading.
+# uploading file.bin
+http -a {{user}}:password PUT https://central.artipie.com/{{user}}/{{repo}}file.bin @file.bin
+# downloading file.bin
+http GET https://central.artipie.com/{{user}}/{{repo}}file.bin --output=./file.bin
+
\ No newline at end of file
diff --git a/src/main/resources/info/gem.info.html b/src/main/resources/info/gem.info.html
new file mode 100644
index 0000000..8f8f215
--- /dev/null
+++ b/src/main/resources/info/gem.info.html
@@ -0,0 +1,13 @@
+Before uploading your gems, obtain a key for authorization. A base64 encoded
+login:password would be a valid key:
+
\ No newline at end of file
diff --git a/src/main/resources/info/go.info.html b/src/main/resources/info/go.info.html
new file mode 100644
index 0000000..6d4cf91
--- /dev/null
+++ b/src/main/resources/info/go.info.html
@@ -0,0 +1,9 @@
+To use go repository declare the following environment variables:
+
+Now you can install packages directly from Artipie:
+
+go get -x -insecure golang.org/x/time
+
\ No newline at end of file
diff --git a/src/main/resources/info/helm.info.html b/src/main/resources/info/helm.info.html
new file mode 100644
index 0000000..241aa08
--- /dev/null
+++ b/src/main/resources/info/helm.info.html
@@ -0,0 +1,9 @@
+To install chart from Artipie, add repository url to helm and run helm install:
+
+ curl -i -X POST --data-binary "@my-chart.tgz" https://central.artipie.com/{{user}}/{{repo}}
+
\ No newline at end of file
diff --git a/src/main/resources/info/maven-group.info.html b/src/main/resources/info/maven-group.info.html
new file mode 100644
index 0000000..38fb038
--- /dev/null
+++ b/src/main/resources/info/maven-group.info.html
@@ -0,0 +1,8 @@
+
To use Maven grouped (virtual) repository first create at least two Maven or Maven proxy repositories.
+It can be either just a Maven repository or Maven proxy (mirror) repository. Then specify
+https://central.artipie.com/{{user}}/{{repo}} as distribution management, or
+repositories in pom.xml or as a mirror in settings.xml (see
+more details in concrete repositories documentation). Then group repositories under
+group-settings, pay attention that order does matter: first repository in the list will be
+accessed first, then second, etc. Repository name should be fully formatted, include your username
+prefix, e.g. {{user}}/maven.
\ No newline at end of file
diff --git a/src/main/resources/info/maven-proxy.info.html b/src/main/resources/info/maven-proxy.info.html
new file mode 100644
index 0000000..38717c6
--- /dev/null
+++ b/src/main/resources/info/maven-proxy.info.html
@@ -0,0 +1,18 @@
+
Maven proxy repository proxies all request to remote Maven repository appling authentication
+and saves artifacts in Artipie storage cache.
+
To use it as a mirror for Apache central, add this configuration to your
+~/.m2/settings.xml:
+
+
\ No newline at end of file
diff --git a/src/main/resources/info/maven.info.html b/src/main/resources/info/maven.info.html
new file mode 100644
index 0000000..78032fc
--- /dev/null
+++ b/src/main/resources/info/maven.info.html
@@ -0,0 +1,31 @@
+
With this confirmation,
+the GitHub user @{{user}}
+will be able to publish Maven artifacts and all other users will be able to download.
+
+
This is how you may configure it inside your
+pom.xml:
You publish just with
+mvn deploy
+and you download with
+mvn compile.
\ No newline at end of file
diff --git a/src/main/resources/info/npm-proxy.info.html b/src/main/resources/info/npm-proxy.info.html
new file mode 100644
index 0000000..e69de29
diff --git a/src/main/resources/info/npm.info.html b/src/main/resources/info/npm.info.html
new file mode 100644
index 0000000..50be411
--- /dev/null
+++ b/src/main/resources/info/npm.info.html
@@ -0,0 +1,6 @@
+To install or publish npm package into Artipie repository, specify the repository url
+with the --registry option:
+
\ No newline at end of file
diff --git a/src/main/resources/info/nuget.info.html b/src/main/resources/info/nuget.info.html
new file mode 100644
index 0000000..a4588d8
--- /dev/null
+++ b/src/main/resources/info/nuget.info.html
@@ -0,0 +1,21 @@
+To install or publish NuGet package into Artipie, specify repository url
+and credentials in NuGet.Config xml file:
+
\ No newline at end of file
diff --git a/src/main/resources/info/php-proxy.info.html b/src/main/resources/info/php-proxy.info.html
new file mode 100644
index 0000000..e69de29
diff --git a/src/main/resources/info/php.info.html b/src/main/resources/info/php.info.html
new file mode 100644
index 0000000..fe7bdf0
--- /dev/null
+++ b/src/main/resources/info/php.info.html
@@ -0,0 +1,26 @@
+To publish your PHP Composer package create package description JSON file my-package.json with the following content:
+
\ No newline at end of file
diff --git a/src/main/resources/info/rpm.info.html b/src/main/resources/info/rpm.info.html
new file mode 100644
index 0000000..ec76bca
--- /dev/null
+++ b/src/main/resources/info/rpm.info.html
@@ -0,0 +1,14 @@
+To install packages from rpm Artipie repository, add the following file
+to yum settings by path /etc/yum.repos.d/example.repo:
+