fix #920 Admin API: start/stop scheduled job

This commit is contained in:
Shinsuke Sugaya 2017-03-10 06:28:06 +09:00
parent 68ae9a3b79
commit e515dffdb1
14 changed files with 384 additions and 45 deletions

View file

@ -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();
}
}

View file

@ -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.codelibs.fess.util.ComponentUtil;
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;
return this;
}
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(" "));
return this;
}
}
}

View file

@ -0,0 +1,75 @@
/*
* 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 javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
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.action.FessMessages;
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;
import org.lastaflute.web.validation.VaMessenger;
public abstract class FessApiAction extends FessBaseAction {
@Resource
protected FessConfig fessConfig;
@Resource
protected MessageManager messageManager;
@Resource
protected AccessTokenService accessTokenService;
@Resource
protected HttpServletRequest request;
@Override
protected OptionalThing<LoginManager> myLoginManager() {
return OptionalThing.empty();
}
@Override
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();
validationMessagesLambda.message(messages);
return messageManager.toMessageList(request.getLocale() == null ? Locale.ENGLISH : request.getLocale(), messages).stream()
.collect(Collectors.joining(" "));
}
protected boolean isAccessAllowed() {
return false;
}
}

View file

@ -0,0 +1,40 @@
/*
* 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.admin;
import org.codelibs.fess.app.web.api.FessApiAction;
import org.codelibs.fess.exception.InvalidAccessTokenException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public abstract class FessApiAdminAction extends FessApiAction {
private static final Logger logger = LoggerFactory.getLogger(FessApiAdminAction.class);
@Override
protected boolean isAccessAllowed() {
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);
}
return false;
}
}
}

View file

@ -0,0 +1,82 @@
/*
* 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.admin.scheduler;
import javax.annotation.Resource;
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.ApiResult.Status;
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 {
@Resource
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());
});
}
try {
entity.start();
} catch (final Exception e) {
throwValidationErrorApi(messages -> {
messages.addErrorsFailedToStartJob(GLOBAL, entity.getName());
});
}
}).orElse(() -> {
throwValidationErrorApi(messages -> {
messages.addErrorsFailedToStartJob(GLOBAL, id);
});
});
return asJson(new ApiResponse().status(Status.OK).result());
}
// POST /api/admin/scheduler/{id}/stop
@Execute(urlPattern = "{}/@word")
public JsonResponse<ApiResult> post$stop(String id) {
scheduledJobService.getScheduledJob(id).ifPresent(entity -> {
try {
entity.stop();
} catch (final Exception e) {
throwValidationErrorApi(messages -> {
messages.addErrorsFailedToStopJob(GLOBAL, entity.getName());
});
}
}).orElse(() -> {
throwValidationErrorApi(messages -> {
messages.addErrorsFailedToStartJob(GLOBAL, id);
});
});
return asJson(new ApiResponse().status(Status.OK).result());
}
}

View file

@ -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.mylasta.direction.FessConfig;
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) {

View file

@ -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");
}

View file

@ -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));
return this;
}
/**
* Add the created action message for the key 'errors.invalid_query_unknown' with parameters.
* <pre>

View file

@ -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);
}

View file

@ -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 {
}
String getApiAdminAccessPermissions();
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));
}
}

View file

@ -15,8 +15,11 @@
*/
package org.codelibs.fess.mylasta.direction.sponsor;
import java.util.List;
import java.util.stream.Collectors;
import org.codelibs.fess.app.web.api.ApiResult;
import org.codelibs.fess.app.web.api.ApiResult.ApiErrorResponse;
import org.codelibs.fess.app.web.api.ApiResult.Status;
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);
}
@Override
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";
}
}

View file

@ -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

View file

@ -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.

View file

@ -125,6 +125,7 @@ errors.failed_to_upgrade_from=Failed to upgrade from {0}.
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.