@@ -15,17 +15,26 @@
*/
package org.codelibs.fess.app.service;
+import static org.codelibs.core.stream.StreamUtil.stream;
+
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
import javax.annotation.Resource;
+import javax.servlet.http.HttpServletRequest;
import org.codelibs.core.beans.util.BeanUtil;
+import org.codelibs.core.lang.StringUtil;
import org.codelibs.fess.Constants;
import org.codelibs.fess.app.pager.AccessTokenPager;
import org.codelibs.fess.es.config.cbean.AccessTokenCB;
import org.codelibs.fess.es.config.exbhv.AccessTokenBhv;
import org.codelibs.fess.es.config.exentity.AccessToken;
+import org.codelibs.fess.exception.InvalidAccessTokenException;
import org.codelibs.fess.mylasta.direction.FessConfig;
+import org.codelibs.fess.taglib.FessFunctions;
+import org.codelibs.fess.util.ComponentUtil;
import org.dbflute.cbean.result.PagingResultBean;
import org.dbflute.optional.OptionalEntity;
@@ -82,10 +91,28 @@ public class AccessTokenService {
}
- public OptionalEntity<AccessToken> getAccessTokenByToken(final String token) {
- return accessTokenBhv.selectEntity(cb -> {
- cb.query().setToken_Term(token);
- });
+ public OptionalEntity<Set<String>> getPermissions(final HttpServletRequest request) {
+ final String token = request.getHeader("Authorization");
+ if (StringUtil.isNotBlank(token)) {
+ return accessTokenBhv
+ .selectEntity(cb -> {
+ cb.query().setToken_Term(token);
+ })
+ .map(accessToken -> {
+ final Set<String> permissionSet = new HashSet<>();
+ final Long expiredTime = accessToken.getExpiredTime();
+ if (expiredTime != null && expiredTime.longValue() > 0
+ && expiredTime.longValue() < ComponentUtil.getSystemHelper().getCurrentTimeAsLong()) {
+ throw new InvalidAccessTokenException("invalid_token", "The token is expired("
+ + FessFunctions.formatDate(FessFunctions.date(expiredTime)) + ").");
+ }
+ stream(accessToken.getPermissions()).of(stream -> stream.forEach(permissionSet::add));
+ final String name = accessToken.getParameterName();
+ stream(request.getParameterValues(name)).of(
+ stream -> stream.filter(StringUtil::isNotBlank).forEach(permissionSet::add));
+ return OptionalEntity.of(permissionSet);
+ }).orElseThrow(() -> new InvalidAccessTokenException("invalid_token", "Invalid token: " + token));
+ return OptionalEntity.empty();
-
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2012-2017 CodeLibs Project and the Others.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+package org.codelibs.fess.app.web.api;
+import java.util.Locale;
+import java.util.stream.Collectors;
+import org.codelibs.fess.Constants;
+import org.codelibs.fess.mylasta.action.FessMessages;
+import org.lastaflute.web.util.LaRequestUtil;
+import org.lastaflute.web.validation.VaMessenger;
+public class ApiResult {
+ protected ApiResponse response = null;
+ public ApiResult(ApiResponse response) {
+ this.response = response;
+ public enum Status {
+ OK(0), BAD_REQUEST(1), SYSTEM_ERROR(2), UNAUTHORIZED(3);
+ private final int id;
+ private Status(final int id) {
+ this.id = id;
+ public int getId() {
+ return id;
+ public static class ApiResponse {
+ protected String version = Constants.WEB_API_VERSION;
+ protected int status;
+ public ApiResponse status(Status status) {
+ this.status = status.getId();
+ return this;
+ public ApiResult result() {
+ return new ApiResult(this);
+ public static class ApiErrorResponse extends ApiResponse {
+ protected String message;
+ public ApiErrorResponse message(String message) {
+ this.message = message;
+ public ApiErrorResponse message(VaMessenger<FessMessages> validationMessagesLambda) {
+ FessMessages messages = new FessMessages();
+ validationMessagesLambda.message(messages);
+ message =
+ ComponentUtil.getMessageManager()
+ .toMessageList(LaRequestUtil.getOptionalRequest().map(r -> r.getLocale()).orElse(Locale.ENGLISH), messages)
+ .stream().collect(Collectors.joining(" "));
+}
@@ -0,0 +1,75 @@
+import javax.annotation.Resource;
+import org.codelibs.fess.app.service.AccessTokenService;
+import org.codelibs.fess.app.web.api.ApiResult.ApiErrorResponse;
+import org.codelibs.fess.app.web.api.ApiResult.Status;
+import org.codelibs.fess.app.web.base.FessBaseAction;
+import org.codelibs.fess.mylasta.direction.FessConfig;
+import org.dbflute.optional.OptionalThing;
+import org.lastaflute.core.message.MessageManager;
+import org.lastaflute.web.login.LoginManager;
+import org.lastaflute.web.response.ActionResponse;
+import org.lastaflute.web.ruts.process.ActionRuntime;
+public abstract class FessApiAction extends FessBaseAction {
+ @Resource
+ protected FessConfig fessConfig;
+ protected MessageManager messageManager;
+ protected AccessTokenService accessTokenService;
+ protected HttpServletRequest request;
+ @Override
+ protected OptionalThing<LoginManager> myLoginManager() {
+ return OptionalThing.empty();
+ public ActionResponse godHandPrologue(final ActionRuntime runtime) {
+ if (!isAccessAllowed()) {
+ return asJson(new ApiErrorResponse().message(getMessage(messages -> messages.addErrorsUnauthorizedRequest(GLOBAL)))
+ .status(Status.UNAUTHORIZED).result());
+ return super.godHandPrologue(runtime);
+ protected String getMessage(VaMessenger<FessMessages> validationMessagesLambda) {
+ final FessMessages messages = new FessMessages();
+ return messageManager.toMessageList(request.getLocale() == null ? Locale.ENGLISH : request.getLocale(), messages).stream()
+ .collect(Collectors.joining(" "));
+ protected boolean isAccessAllowed() {
+ return false;
@@ -0,0 +1,40 @@
+package org.codelibs.fess.app.web.api.admin;
+import org.codelibs.fess.app.web.api.FessApiAction;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+public abstract class FessApiAdminAction extends FessApiAction {
+ private static final Logger logger = LoggerFactory.getLogger(FessApiAdminAction.class);
+ try {
+ return accessTokenService.getPermissions(request).map(permissions -> {
+ return fessConfig.isApiAdminAccessAllowed(permissions);
+ }).orElse(false);
+ } catch (InvalidAccessTokenException e) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Invalid access token.", e);
@@ -0,0 +1,82 @@
+package org.codelibs.fess.app.web.api.admin.scheduler;
+import org.codelibs.fess.app.service.ScheduledJobService;
+import org.codelibs.fess.app.web.api.ApiResult;
+import org.codelibs.fess.app.web.api.ApiResult.ApiResponse;
+import org.codelibs.fess.app.web.api.admin.FessApiAdminAction;
+import org.lastaflute.web.Execute;
+import org.lastaflute.web.response.HtmlResponse;
+import org.lastaflute.web.response.JsonResponse;
+public class ApiAdminSchedulerAction extends FessApiAdminAction {
+ private ScheduledJobService scheduledJobService;
+ @Execute
+ public HtmlResponse index() {
+ throw new UnsupportedOperationException();
+ // POST /api/admin/scheduler/{id}/start
+ @Execute(urlPattern = "{}/@word")
+ public JsonResponse<ApiResult> post$start(String id) {
+ scheduledJobService.getScheduledJob(id).ifPresent(entity -> {
+ if (!entity.isEnabled() || entity.isRunning()) {
+ throwValidationErrorApi(messages -> {
+ messages.addErrorsFailedToStartJob(GLOBAL, entity.getName());
+ });
+ entity.start();
+ } catch (final Exception e) {
+ }).orElse(() -> {
+ messages.addErrorsFailedToStartJob(GLOBAL, id);
+ return asJson(new ApiResponse().status(Status.OK).result());
+ // POST /api/admin/scheduler/{id}/stop
+ public JsonResponse<ApiResult> post$stop(String id) {
+ entity.stop();
+ messages.addErrorsFailedToStopJob(GLOBAL, entity.getName());
@@ -35,7 +35,6 @@ import org.codelibs.fess.entity.SearchRequestParams.SearchRequestType;
import org.codelibs.fess.exception.InvalidAccessTokenException;
import org.codelibs.fess.mylasta.action.FessUserBean;
-import org.codelibs.fess.taglib.FessFunctions;
import org.codelibs.fess.util.ComponentUtil;
import org.lastaflute.web.servlet.request.RequestManager;
import org.lastaflute.web.util.LaRequestUtil;
@@ -158,28 +157,7 @@ public class RoleQueryHelper {
protected void processAccessToken(final HttpServletRequest request, final Set<String> roleSet) {
- final String token = request.getHeader("Authorization");
- if (StringUtil.isNotBlank(token)) {
- final AccessTokenService accessTokenService = ComponentUtil.getComponent(AccessTokenService.class);
- accessTokenService
- .getAccessTokenByToken(token)
- .ifPresent(
- accessToken -> {
- final Long expiredTime = accessToken.getExpiredTime();
- if (expiredTime != null && expiredTime.longValue() > 0
- && expiredTime.longValue() < ComponentUtil.getSystemHelper().getCurrentTimeAsLong()) {
- throw new InvalidAccessTokenException("invalid_token", "The token is expired("
- + FessFunctions.formatDate(FessFunctions.date(expiredTime)) + ").");
- }
- stream(accessToken.getPermissions()).of(stream -> stream.forEach(roleSet::add));
- final String name = accessToken.getParameterName();
- stream(request.getParameterValues(name)).of(
- stream -> stream.filter(StringUtil::isNotBlank).forEach(roleSet::add));
- }).orElse(() -> {
- throw new InvalidAccessTokenException("invalid_token", "Invalid token: " + token);
+ ComponentUtil.getComponent(AccessTokenService.class).getPermissions(request).ifPresent(p -> p.forEach(roleSet::add));
protected String getAccessToken(final HttpServletRequest request) {
@@ -387,6 +387,9 @@ public interface FessHtmlPath {
/** The path of the HTML: /searchNoResult.jsp */
HtmlNext path_SearchNoResultJsp = new HtmlNext("/searchNoResult.jsp");
+ /** The path of the HTML: /searchOptions.jsp */
+ HtmlNext path_SearchOptionsJsp = new HtmlNext("/searchOptions.jsp");
/** The path of the HTML: /searchResults.jsp */
HtmlNext path_SearchResultsJsp = new HtmlNext("/searchResults.jsp");
@@ -323,6 +323,9 @@ public class FessMessages extends FessLabels {
/** The key of the message: Could not delete logged in user. */
public static final String ERRORS_could_not_delete_logged_in_user = "{errors.could_not_delete_logged_in_user}";
+ /** The key of the message: Unauthorized request. */
+ public static final String ERRORS_unauthorized_request = "{errors.unauthorized_request}";
/** The key of the message: The given query has unknown condition. */
public static final String ERRORS_invalid_query_unknown = "{errors.invalid_query_unknown}";
@@ -1886,6 +1889,20 @@ public class FessMessages extends FessLabels {
return this;
+ /**
+ * Add the created action message for the key 'errors.unauthorized_request' with parameters.
+ * <pre>
+ * message: Unauthorized request.
+ * </pre>
+ * @param property The property name for the message. (NotNull)
+ * @return this. (NotNull)
+ public FessMessages addErrorsUnauthorizedRequest(String property) {
+ assertPropertyNotNull(property);
+ add(property, new UserMessage(ERRORS_unauthorized_request));
/**
* Add the created action message for the key 'errors.invalid_query_unknown' with parameters.
* <pre>
@@ -133,6 +133,9 @@ public interface FessConfig extends FessEnv, org.codelibs.fess.mylasta.direction
/** The key of the configuration. e.g. false */
String API_ACCESS_TOKEN_REQUIRED = "api.access.token.required";
+ /** The key of the configuration. e.g. Radmin-api */
+ String API_ADMIN_ACCESS_PERMISSIONS = "api.admin.access.permissions";
/** The key of the configuration. e.g. 50 */
String CRAWLER_DOCUMENT_MAX_SITE_LENGTH = "crawler.document.max.site.length";
@@ -1402,6 +1405,13 @@ public interface FessConfig extends FessEnv, org.codelibs.fess.mylasta.direction
boolean isApiAccessTokenRequired();
+ * Get the value for the key 'api.admin.access.permissions'. <br>
+ * The value is, e.g. Radmin-api <br>
+ * @return The value of found property. (NotNull: if not found, exception but basically no way)
+ String getApiAdminAccessPermissions();
* Get the value for the key 'crawler.document.max.site.length'. <br>
* The value is, e.g. 50 <br>
@@ -4798,6 +4808,10 @@ public interface FessConfig extends FessEnv, org.codelibs.fess.mylasta.direction
return is(FessConfig.API_ACCESS_TOKEN_REQUIRED);
+ public String getApiAdminAccessPermissions() {
+ return get(FessConfig.API_ADMIN_ACCESS_PERMISSIONS);
public String getCrawlerDocumentMaxSiteLength() {
return get(FessConfig.CRAWLER_DOCUMENT_MAX_SITE_LENGTH);
@@ -58,6 +58,8 @@ import org.lastaflute.web.validation.theme.typed.LongTypeValidator;
public interface FessProp {
+ public static final String API_ADMIN_ACCESS_PERMISSION_SET = "apiAdminAccessPermissionSet";
public static final String CRAWLER_DOCUMENT_SPACE_CHARS = "crawlerDocumentSpaceChars";
public static final String INDEX_ADMIN_ARRAY_FIELD_SET = "indexAdminArrayFieldSet";
@@ -1469,4 +1471,21 @@ public interface FessProp {
+ public default Set<String> getApiAdminAccessPermissionSet() {
+ @SuppressWarnings("unchecked")
+ Set<String> fieldSet = (Set<String>) propMap.get(API_ADMIN_ACCESS_PERMISSION_SET);
+ if (fieldSet == null) {
+ fieldSet =
+ split(getApiAdminAccessPermissions(), ",").get(
+ stream -> stream.filter(StringUtil::isNotBlank).map(s -> s.trim()).collect(Collectors.toSet()));
+ propMap.put(API_ADMIN_ACCESS_PERMISSION_SET, fieldSet);
+ return fieldSet;
+ public default boolean isApiAdminAccessAllowed(final Set<String> accessPermissions) {
+ return getApiAdminAccessPermissionSet().stream().anyMatch(s -> accessPermissions.contains(s));
@@ -15,8 +15,11 @@
package org.codelibs.fess.mylasta.direction.sponsor;
-import java.util.List;
import org.dbflute.optional.OptionalThing;
import org.lastaflute.web.api.ApiFailureHook;
import org.lastaflute.web.api.ApiFailureResource;
@@ -40,18 +43,16 @@ public class FessApiFailureHook implements ApiFailureHook { // #change_it for ha
// ================
@Override
public ApiResponse handleValidationError(final ApiFailureResource resource) {
- return asJson(createFailureBean(resource)).httpStatus(HTTP_BAD_REQUEST);
+ return asJson(createFailureBean(Status.BAD_REQUEST, createMessage(resource, null))).httpStatus(HTTP_BAD_REQUEST);
public ApiResponse handleApplicationException(final ApiFailureResource resource, final RuntimeException cause) {
- final int status;
if (cause instanceof LoginUnauthorizedException) {
- status = HTTP_UNAUTHORIZED; // unauthorized
+ return asJson(createFailureBean(Status.UNAUTHORIZED, "Unauthorized request.")).httpStatus(HTTP_UNAUTHORIZED);
} else {
- status = HTTP_BAD_REQUEST; // bad request
+ return asJson(createFailureBean(Status.BAD_REQUEST, createMessage(resource, cause))).httpStatus(HTTP_BAD_REQUEST);
- return asJson(createFailureBean(resource)).httpStatus(status);
// ===================================================================================
@@ -70,22 +71,22 @@ public class FessApiFailureHook implements ApiFailureHook { // #change_it for ha
// Assist Logic
// ============
- protected JsonResponse<TooSimpleFailureBean> asJson(final TooSimpleFailureBean bean) {
+ protected JsonResponse<ApiResult> asJson(final ApiResult bean) {
return new JsonResponse<>(bean);
- protected TooSimpleFailureBean createFailureBean(final ApiFailureResource resource) {
- return new TooSimpleFailureBean(resource.getMessageList());
+ protected ApiResult createFailureBean(final Status status, final String message) {
+ return new ApiErrorResponse().message(message).status(status).result();
- public static class TooSimpleFailureBean {
- public final String notice = "[Attension] tentative JSON so you should change it: " + FessApiFailureHook.class;
- public final List<String> messageList;
- public TooSimpleFailureBean(final List<String> messageList) {
- this.messageList = messageList;
+ protected String createMessage(final ApiFailureResource resource, final RuntimeException cause) {
+ if (!resource.getMessageList().isEmpty()) {
+ return resource.getMessageList().stream().collect(Collectors.joining(" "));
+ if (cause != null) {
+ return cause.getMessage();
+ return "Unknown error";
@@ -78,6 +78,7 @@ supported.uploaded.files=license.properties
supported.languages=ar,bg,ca,da,de,el,en,es,eu,fa,fi,fr,ga,gl,hi,hu,hy,id,it,ja,lv,ko,nl,no,pt,ro,ru,sv,th,tr,zh_CN,zh_TW,zh
api.access.token.length=60
api.access.token.required=false
+api.admin.access.permissions=Radmin-api
# ========================================================================================
# Index
@@ -129,6 +129,7 @@ errors.failed_to_upgrade_from=Failed to upgrade from {0}: {1}
errors.failed_to_read_request_file=Failed to read request file: {0}
errors.invalid_header_for_request_file=Invalid header: {0}
errors.could_not_delete_logged_in_user=Could not delete logged in user.
+errors.unauthorized_request=Unauthorized request.
errors.invalid_query_unknown=The given query has unknown condition.
errors.invalid_query_parse_error=The given query is invalid.
@@ -125,6 +125,7 @@ errors.failed_to_upgrade_from=Failed to upgrade from {0}.