diff options
author | casswarry0 <casswarry0@gmail.com> | 2023-01-17 17:14:53 -0700 |
---|---|---|
committer | casswarry0 <casswarry0@gmail.com> | 2023-01-17 17:14:53 -0700 |
commit | 7338b8730949ed027b3f8b31d7dca20687ebbb8b (patch) | |
tree | c2d171e6121818ab511460a786f69aab97a2a628 /src/main/java/org/traccar/api | |
parent | cdecd3fa4427a382c0b09f8ad9d69ec14388960a (diff) | |
parent | 85501f9cf4918d5eee345f83aed7a31eecb26b8d (diff) | |
download | trackermap-server-7338b8730949ed027b3f8b31d7dca20687ebbb8b.tar.gz trackermap-server-7338b8730949ed027b3f8b31d7dca20687ebbb8b.tar.bz2 trackermap-server-7338b8730949ed027b3f8b31d7dca20687ebbb8b.zip |
Merge branch 'master' into develop
Diffstat (limited to 'src/main/java/org/traccar/api')
31 files changed, 1685 insertions, 710 deletions
diff --git a/src/main/java/org/traccar/api/AsyncSocket.java b/src/main/java/org/traccar/api/AsyncSocket.java index b1853822d..5fc4b4412 100644 --- a/src/main/java/org/traccar/api/AsyncSocket.java +++ b/src/main/java/org/traccar/api/AsyncSocket.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 - 2021 Anton Tananaev (anton@traccar.org) + * Copyright 2015 - 2022 Anton Tananaev (anton@traccar.org) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,15 +16,18 @@ package org.traccar.api; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import org.eclipse.jetty.websocket.api.Session; import org.eclipse.jetty.websocket.api.WebSocketAdapter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.traccar.Context; -import org.traccar.database.ConnectionManager; +import org.traccar.helper.model.PositionUtil; +import org.traccar.session.ConnectionManager; import org.traccar.model.Device; import org.traccar.model.Event; import org.traccar.model.Position; +import org.traccar.storage.Storage; +import org.traccar.storage.StorageException; import java.util.Collection; import java.util.Collections; @@ -39,9 +42,15 @@ public class AsyncSocket extends WebSocketAdapter implements ConnectionManager.U private static final String KEY_POSITIONS = "positions"; private static final String KEY_EVENTS = "events"; + private final ObjectMapper objectMapper; + private final ConnectionManager connectionManager; + private final Storage storage; private final long userId; - public AsyncSocket(long userId) { + public AsyncSocket(ObjectMapper objectMapper, ConnectionManager connectionManager, Storage storage, long userId) { + this.objectMapper = objectMapper; + this.connectionManager = connectionManager; + this.storage = storage; this.userId = userId; } @@ -49,18 +58,21 @@ public class AsyncSocket extends WebSocketAdapter implements ConnectionManager.U public void onWebSocketConnect(Session session) { super.onWebSocketConnect(session); - Map<String, Collection<?>> data = new HashMap<>(); - data.put(KEY_POSITIONS, Context.getDeviceManager().getInitialState(userId)); - sendData(data); - - Context.getConnectionManager().addListener(userId, this); + try { + Map<String, Collection<?>> data = new HashMap<>(); + data.put(KEY_POSITIONS, PositionUtil.getLatestPositions(storage, userId)); + sendData(data); + connectionManager.addListener(userId, this); + } catch (StorageException e) { + throw new RuntimeException(e); + } } @Override public void onWebSocketClose(int statusCode, String reason) { super.onWebSocketClose(statusCode, reason); - Context.getConnectionManager().removeListener(userId, this); + connectionManager.removeListener(userId, this); } @Override @@ -92,7 +104,7 @@ public class AsyncSocket extends WebSocketAdapter implements ConnectionManager.U private void sendData(Map<String, Collection<?>> data) { if (isConnected()) { try { - getRemote().sendString(Context.getObjectMapper().writeValueAsString(data), null); + getRemote().sendString(objectMapper.writeValueAsString(data), null); } catch (JsonProcessingException e) { LOGGER.warn("Socket JSON formatting error", e); } diff --git a/src/main/java/org/traccar/api/AsyncSocketServlet.java b/src/main/java/org/traccar/api/AsyncSocketServlet.java index a964ead10..91a745eeb 100644 --- a/src/main/java/org/traccar/api/AsyncSocketServlet.java +++ b/src/main/java/org/traccar/api/AsyncSocketServlet.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 - 2021 Anton Tananaev (anton@traccar.org) + * Copyright 2015 - 2022 Anton Tananaev (anton@traccar.org) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,27 +15,48 @@ */ package org.traccar.api; +import com.fasterxml.jackson.databind.ObjectMapper; import org.eclipse.jetty.websocket.server.JettyWebSocketServlet; import org.eclipse.jetty.websocket.server.JettyWebSocketServletFactory; -import org.traccar.Context; import org.traccar.api.resource.SessionResource; +import org.traccar.config.Config; import org.traccar.config.Keys; +import org.traccar.session.ConnectionManager; +import org.traccar.storage.Storage; +import javax.inject.Inject; +import javax.inject.Singleton; import javax.servlet.http.HttpSession; import java.time.Duration; +@Singleton public class AsyncSocketServlet extends JettyWebSocketServlet { + private final Config config; + private final ObjectMapper objectMapper; + private final ConnectionManager connectionManager; + private final Storage storage; + + @Inject + public AsyncSocketServlet( + Config config, ObjectMapper objectMapper, ConnectionManager connectionManager, Storage storage) { + this.config = config; + this.objectMapper = objectMapper; + this.connectionManager = connectionManager; + this.storage = storage; + } + @Override public void configure(JettyWebSocketServletFactory factory) { - factory.setIdleTimeout(Duration.ofMillis(Context.getConfig().getLong(Keys.WEB_TIMEOUT))); + factory.setIdleTimeout(Duration.ofMillis(config.getLong(Keys.WEB_TIMEOUT))); factory.setCreator((req, resp) -> { if (req.getSession() != null) { - long userId = (Long) ((HttpSession) req.getSession()).getAttribute(SessionResource.USER_ID_KEY); - return new AsyncSocket(userId); - } else { - return null; + Long userId = (Long) ((HttpSession) req.getSession()).getAttribute(SessionResource.USER_ID_KEY); + if (userId != null) { + return new AsyncSocket(objectMapper, connectionManager, storage, userId); + } } + return null; }); } diff --git a/src/main/java/org/traccar/api/BaseObjectResource.java b/src/main/java/org/traccar/api/BaseObjectResource.java index 71f3939cb..904781e54 100644 --- a/src/main/java/org/traccar/api/BaseObjectResource.java +++ b/src/main/java/org/traccar/api/BaseObjectResource.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 - 2020 Anton Tananaev (anton@traccar.org) + * Copyright 2017 - 2022 Anton Tananaev (anton@traccar.org) * Copyright 2017 - 2018 Andrey Kunitsyn (andrey@traccar.org) * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,9 +16,19 @@ */ package org.traccar.api; -import java.sql.SQLException; -import java.util.Set; - +import org.traccar.helper.LogAction; +import org.traccar.model.BaseModel; +import org.traccar.model.Group; +import org.traccar.model.Permission; +import org.traccar.model.User; +import org.traccar.session.ConnectionManager; +import org.traccar.session.cache.CacheManager; +import org.traccar.storage.StorageException; +import org.traccar.storage.query.Columns; +import org.traccar.storage.query.Condition; +import org.traccar.storage.query.Request; + +import javax.inject.Inject; import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.POST; @@ -27,58 +37,26 @@ import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.core.Response; -import org.traccar.Context; -import org.traccar.database.BaseObjectManager; -import org.traccar.database.ExtendedObjectManager; -import org.traccar.database.ManagableObjects; -import org.traccar.database.SimpleObjectManager; -import org.traccar.helper.LogAction; -import org.traccar.model.BaseModel; -import org.traccar.model.Calendar; -import org.traccar.model.Command; -import org.traccar.model.Device; -import org.traccar.model.Group; -import org.traccar.model.GroupedModel; -import org.traccar.model.ScheduledModel; -import org.traccar.model.User; - public abstract class BaseObjectResource<T extends BaseModel> extends BaseResource { - private final Class<T> baseClass; + @Inject + private CacheManager cacheManager; - public BaseObjectResource(Class<T> baseClass) { - this.baseClass = baseClass; - } + @Inject + private ConnectionManager connectionManager; - protected final Class<T> getBaseClass() { - return baseClass; - } + protected final Class<T> baseClass; - protected final Set<Long> getSimpleManagerItems(BaseObjectManager<T> manager, boolean all, long userId) { - Set<Long> result; - if (all) { - if (Context.getPermissionsManager().getUserAdmin(getUserId())) { - result = manager.getAllItems(); - } else { - Context.getPermissionsManager().checkManager(getUserId()); - result = ((ManagableObjects) manager).getManagedItems(getUserId()); - } - } else { - if (userId == 0) { - userId = getUserId(); - } - Context.getPermissionsManager().checkUser(getUserId(), userId); - result = ((ManagableObjects) manager).getUserItems(userId); - } - return result; + public BaseObjectResource(Class<T> baseClass) { + this.baseClass = baseClass; } @Path("{id}") @GET - public Response getSingle(@PathParam("id") long id) throws SQLException { - Context.getPermissionsManager().checkPermission(baseClass, getUserId(), id); - BaseObjectManager<T> manager = Context.getManager(baseClass); - T entity = manager.getById(id); + public Response getSingle(@PathParam("id") long id) throws StorageException { + permissionsService.checkPermission(baseClass, getUserId(), id); + T entity = storage.getObject(baseClass, new Request( + new Columns.All(), new Condition.Equals("id", id))); if (entity != null) { return Response.ok(entity).build(); } else { @@ -87,103 +65,64 @@ public abstract class BaseObjectResource<T extends BaseModel> extends BaseResour } @POST - public Response add(T entity) throws SQLException { - Context.getPermissionsManager().checkReadonly(getUserId()); - if (baseClass.equals(Device.class)) { - Context.getPermissionsManager().checkDeviceReadonly(getUserId()); - Context.getPermissionsManager().checkDeviceLimit(getUserId()); - } else if (baseClass.equals(Command.class)) { - Context.getPermissionsManager().checkLimitCommands(getUserId()); - } else if (entity instanceof GroupedModel && ((GroupedModel) entity).getGroupId() != 0) { - Context.getPermissionsManager().checkPermission( - Group.class, getUserId(), ((GroupedModel) entity).getGroupId()); - } else if (entity instanceof ScheduledModel && ((ScheduledModel) entity).getCalendarId() != 0) { - Context.getPermissionsManager().checkPermission( - Calendar.class, getUserId(), ((ScheduledModel) entity).getCalendarId()); - } + public Response add(T entity) throws StorageException { + permissionsService.checkEdit(getUserId(), entity, true); - BaseObjectManager<T> manager = Context.getManager(baseClass); - manager.addItem(entity); + entity.setId(storage.addObject(entity, new Request(new Columns.Exclude("id")))); LogAction.create(getUserId(), entity); - - Context.getDataManager().linkObject(User.class, getUserId(), baseClass, entity.getId(), true); + storage.addPermission(new Permission(User.class, getUserId(), baseClass, entity.getId())); + cacheManager.invalidatePermission(true, User.class, getUserId(), baseClass, entity.getId()); + connectionManager.invalidatePermission(true, User.class, getUserId(), baseClass, entity.getId()); LogAction.link(getUserId(), User.class, getUserId(), baseClass, entity.getId()); - if (manager instanceof SimpleObjectManager) { - ((SimpleObjectManager<T>) manager).refreshUserItems(); - } else if (baseClass.equals(Group.class) || baseClass.equals(Device.class)) { - Context.getPermissionsManager().refreshDeviceAndGroupPermissions(); - Context.getPermissionsManager().refreshAllExtendedPermissions(); - } return Response.ok(entity).build(); } @Path("{id}") @PUT - public Response update(T entity) throws SQLException { - Context.getPermissionsManager().checkReadonly(getUserId()); - if (baseClass.equals(Device.class)) { - Context.getPermissionsManager().checkDeviceReadonly(getUserId()); - } else if (baseClass.equals(User.class)) { - User before = Context.getPermissionsManager().getUser(entity.getId()); - Context.getPermissionsManager().checkUserUpdate(getUserId(), before, (User) entity); - } else if (baseClass.equals(Command.class)) { - Context.getPermissionsManager().checkLimitCommands(getUserId()); - } else if (entity instanceof GroupedModel && ((GroupedModel) entity).getGroupId() != 0) { - Context.getPermissionsManager().checkPermission( - Group.class, getUserId(), ((GroupedModel) entity).getGroupId()); - } else if (entity instanceof ScheduledModel && ((ScheduledModel) entity).getCalendarId() != 0) { - Context.getPermissionsManager().checkPermission( - Calendar.class, getUserId(), ((ScheduledModel) entity).getCalendarId()); + public Response update(T entity) throws StorageException { + permissionsService.checkEdit(getUserId(), entity, false); + permissionsService.checkPermission(baseClass, getUserId(), entity.getId()); + + if (entity instanceof User) { + User before = storage.getObject(User.class, new Request( + new Columns.All(), new Condition.Equals("id", entity.getId()))); + permissionsService.checkUserUpdate(getUserId(), before, (User) entity); + } else if (entity instanceof Group) { + Group group = (Group) entity; + if (group.getId() == group.getGroupId()) { + throw new IllegalArgumentException("Cycle in group hierarchy"); + } } - Context.getPermissionsManager().checkPermission(baseClass, getUserId(), entity.getId()); - Context.getManager(baseClass).updateItem(entity); + storage.updateObject(entity, new Request( + new Columns.Exclude("id"), + new Condition.Equals("id", entity.getId()))); + if (entity instanceof User) { + User user = (User) entity; + if (user.getHashedPassword() != null) { + storage.updateObject(entity, new Request( + new Columns.Include("hashedPassword", "salt"), + new Condition.Equals("id", entity.getId()))); + } + } + cacheManager.updateOrInvalidate(true, entity); LogAction.edit(getUserId(), entity); - if (baseClass.equals(Group.class) || baseClass.equals(Device.class)) { - Context.getPermissionsManager().refreshDeviceAndGroupPermissions(); - Context.getPermissionsManager().refreshAllExtendedPermissions(); - } return Response.ok(entity).build(); } @Path("{id}") @DELETE - public Response remove(@PathParam("id") long id) throws SQLException { - Context.getPermissionsManager().checkReadonly(getUserId()); - if (baseClass.equals(Device.class)) { - Context.getPermissionsManager().checkDeviceReadonly(getUserId()); - } else if (baseClass.equals(Command.class)) { - Context.getPermissionsManager().checkLimitCommands(getUserId()); - } - Context.getPermissionsManager().checkPermission(baseClass, getUserId(), id); + public Response remove(@PathParam("id") long id) throws StorageException { + permissionsService.checkEdit(getUserId(), baseClass, false); + permissionsService.checkPermission(baseClass, getUserId(), id); + + storage.removeObject(baseClass, new Request(new Condition.Equals("id", id))); + cacheManager.invalidate(baseClass, id); - BaseObjectManager<T> manager = Context.getManager(baseClass); - manager.removeItem(id); LogAction.remove(getUserId(), baseClass, id); - if (manager instanceof SimpleObjectManager) { - ((SimpleObjectManager<T>) manager).refreshUserItems(); - if (manager instanceof ExtendedObjectManager) { - ((ExtendedObjectManager<T>) manager).refreshExtendedPermissions(); - } - } - if (baseClass.equals(Group.class) || baseClass.equals(Device.class) || baseClass.equals(User.class)) { - if (baseClass.equals(Group.class)) { - Context.getGroupsManager().refreshItems(); - Context.getDeviceManager().updateDeviceCache(true); - } - Context.getPermissionsManager().refreshDeviceAndGroupPermissions(); - if (baseClass.equals(User.class)) { - Context.getPermissionsManager().refreshAllUsersPermissions(); - } else { - Context.getPermissionsManager().refreshAllExtendedPermissions(); - } - } else if (baseClass.equals(Calendar.class)) { - Context.getGeofenceManager().refreshItems(); - Context.getNotificationManager().refreshItems(); - } return Response.noContent().build(); } diff --git a/src/main/java/org/traccar/api/BaseResource.java b/src/main/java/org/traccar/api/BaseResource.java index cc272df9c..33abe73fa 100644 --- a/src/main/java/org/traccar/api/BaseResource.java +++ b/src/main/java/org/traccar/api/BaseResource.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 - 2017 Anton Tananaev (anton@traccar.org) + * Copyright 2015 - 2022 Anton Tananaev (anton@traccar.org) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,13 +15,25 @@ */ package org.traccar.api; +import org.traccar.api.security.PermissionsService; +import org.traccar.api.security.UserPrincipal; +import org.traccar.storage.Storage; + +import javax.inject.Inject; +import javax.ws.rs.core.Context; import javax.ws.rs.core.SecurityContext; public class BaseResource { - @javax.ws.rs.core.Context + @Context private SecurityContext securityContext; + @Inject + protected Storage storage; + + @Inject + protected PermissionsService permissionsService; + protected long getUserId() { UserPrincipal principal = (UserPrincipal) securityContext.getUserPrincipal(); if (principal != null) { @@ -29,4 +41,5 @@ public class BaseResource { } return 0; } + } diff --git a/src/main/java/org/traccar/api/CorsResponseFilter.java b/src/main/java/org/traccar/api/CorsResponseFilter.java index 91aea5718..67d0341a1 100644 --- a/src/main/java/org/traccar/api/CorsResponseFilter.java +++ b/src/main/java/org/traccar/api/CorsResponseFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org) + * Copyright 2015 - 2022 Anton Tananaev (anton@traccar.org) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,16 +16,26 @@ package org.traccar.api; import io.netty.handler.codec.http.HttpHeaderNames; -import org.traccar.Context; +import org.traccar.config.Config; import org.traccar.config.Keys; +import javax.inject.Inject; +import javax.inject.Singleton; import javax.ws.rs.container.ContainerRequestContext; import javax.ws.rs.container.ContainerResponseContext; import javax.ws.rs.container.ContainerResponseFilter; import java.io.IOException; +@Singleton public class CorsResponseFilter implements ContainerResponseFilter { + private final String allowed; + + @Inject + public CorsResponseFilter(Config config) { + allowed = config.getString(Keys.WEB_ORIGIN); + } + private static final String ORIGIN_ALL = "*"; private static final String HEADERS_ALL = "origin, content-type, accept, authorization"; private static final String METHODS_ALL = "GET, POST, PUT, DELETE, OPTIONS"; @@ -46,8 +56,6 @@ public class CorsResponseFilter implements ContainerResponseFilter { if (!response.getHeaders().containsKey(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN.toString())) { String origin = request.getHeaderString(HttpHeaderNames.ORIGIN.toString()); - String allowed = Context.getConfig().getString(Keys.WEB_ORIGIN); - if (origin == null) { response.getHeaders().add(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN.toString(), ORIGIN_ALL); } else if (allowed == null || allowed.equals(ORIGIN_ALL) || allowed.contains(origin)) { diff --git a/src/main/java/org/traccar/api/ExtendedObjectResource.java b/src/main/java/org/traccar/api/ExtendedObjectResource.java index 9e554217e..8467b46c6 100644 --- a/src/main/java/org/traccar/api/ExtendedObjectResource.java +++ b/src/main/java/org/traccar/api/ExtendedObjectResource.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Anton Tananaev (anton@traccar.org) + * Copyright 2017 - 2022 Anton Tananaev (anton@traccar.org) * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org) * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,17 +16,19 @@ */ package org.traccar.api; -import java.sql.SQLException; -import java.util.Collection; -import java.util.HashSet; -import java.util.Set; +import org.traccar.model.BaseModel; +import org.traccar.model.Device; +import org.traccar.model.Group; +import org.traccar.model.User; +import org.traccar.storage.StorageException; +import org.traccar.storage.query.Columns; +import org.traccar.storage.query.Condition; +import org.traccar.storage.query.Request; import javax.ws.rs.GET; import javax.ws.rs.QueryParam; - -import org.traccar.Context; -import org.traccar.database.ExtendedObjectManager; -import org.traccar.model.BaseModel; +import java.util.Collection; +import java.util.LinkedList; public class ExtendedObjectResource<T extends BaseModel> extends BaseObjectResource<T> { @@ -36,27 +38,34 @@ public class ExtendedObjectResource<T extends BaseModel> extends BaseObjectResou @GET public Collection<T> get( - @QueryParam("all") boolean all, @QueryParam("userId") long userId, @QueryParam("groupId") long groupId, - @QueryParam("deviceId") long deviceId, @QueryParam("refresh") boolean refresh) throws SQLException { - - ExtendedObjectManager<T> manager = (ExtendedObjectManager<T>) Context.getManager(getBaseClass()); - if (refresh) { - manager.refreshItems(); - } + @QueryParam("all") boolean all, @QueryParam("userId") long userId, + @QueryParam("groupId") long groupId, @QueryParam("deviceId") long deviceId) throws StorageException { - Set<Long> result = new HashSet<>(getSimpleManagerItems(manager, all, userId)); + var conditions = new LinkedList<Condition>(); - if (groupId != 0) { - Context.getPermissionsManager().checkGroup(getUserId(), groupId); - result.retainAll(manager.getGroupItems(groupId)); + if (all) { + if (permissionsService.notAdmin(getUserId())) { + conditions.add(new Condition.Permission(User.class, getUserId(), baseClass)); + } + } else { + if (userId == 0) { + conditions.add(new Condition.Permission(User.class, getUserId(), baseClass)); + } else { + permissionsService.checkUser(getUserId(), userId); + conditions.add(new Condition.Permission(User.class, userId, baseClass).excludeGroups()); + } } - if (deviceId != 0) { - Context.getPermissionsManager().checkDevice(getUserId(), deviceId); - result.retainAll(manager.getDeviceItems(deviceId)); + if (groupId > 0) { + permissionsService.checkPermission(Group.class, getUserId(), groupId); + conditions.add(new Condition.Permission(Group.class, groupId, baseClass).excludeGroups()); + } + if (deviceId > 0) { + permissionsService.checkPermission(Device.class, getUserId(), deviceId); + conditions.add(new Condition.Permission(Device.class, deviceId, baseClass).excludeGroups()); } - return manager.getItems(result); + return storage.getObjects(baseClass, new Request(new Columns.All(), Condition.merge(conditions))); } } diff --git a/src/main/java/org/traccar/api/HealthCheckService.java b/src/main/java/org/traccar/api/HealthCheckService.java deleted file mode 100644 index 0182cc358..000000000 --- a/src/main/java/org/traccar/api/HealthCheckService.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2020 Anton Tananaev (anton@traccar.org) - * - * 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.traccar.api; - -import com.sun.jna.Library; -import com.sun.jna.Native; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.traccar.Context; -import org.traccar.config.Keys; - -import java.util.TimerTask; - -public class HealthCheckService { - - private static final Logger LOGGER = LoggerFactory.getLogger(HealthCheckService.class); - - private SystemD systemD; - - private boolean enabled; - private long period; - - public HealthCheckService() { - if (!Context.getConfig().getBoolean(Keys.WEB_DISABLE_HEALTH_CHECK) - && System.getProperty("os.name").toLowerCase().startsWith("linux")) { - try { - systemD = Native.load("systemd", SystemD.class); - String watchdogTimer = System.getenv("WATCHDOG_USEC"); - if (watchdogTimer != null && !watchdogTimer.isEmpty()) { - period = Long.parseLong(watchdogTimer) / 1000 * 4 / 5; - } - if (period > 0) { - LOGGER.info("Health check enabled with period {}", period); - enabled = true; - } - } catch (UnsatisfiedLinkError e) { - LOGGER.warn("No systemd support", e); - } - } - } - - public boolean isEnabled() { - return enabled; - } - - public long getPeriod() { - return period; - } - - private String getUrl() { - String address = Context.getConfig().getString(Keys.WEB_ADDRESS, "localhost"); - int port = Context.getConfig().getInteger(Keys.WEB_PORT); - return "http://" + address + ":" + port + "/api/server"; - } - - public TimerTask createTask() { - return new TimerTask() { - @Override - public void run() { - LOGGER.debug("Health check running"); - int status = Context.getClient().target(getUrl()).request().get().getStatus(); - if (status == 200) { - int result = systemD.sd_notify(0, "WATCHDOG=1"); - if (result < 0) { - LOGGER.warn("Health check notify error {}", result); - } - } else { - LOGGER.warn("Health check failed with status {}", status); - } - } - }; - } - - interface SystemD extends Library { - @SuppressWarnings("checkstyle:MethodName") - int sd_notify(@SuppressWarnings("checkstyle:ParameterName") int unset_environment, String state); - } - -} diff --git a/src/main/java/org/traccar/api/MediaFilter.java b/src/main/java/org/traccar/api/MediaFilter.java index 77731a810..ab75bdc5d 100644 --- a/src/main/java/org/traccar/api/MediaFilter.java +++ b/src/main/java/org/traccar/api/MediaFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 - 2021 Anton Tananaev (anton@traccar.org) + * Copyright 2018 - 2022 Anton Tananaev (anton@traccar.org) * Copyright 2018 Andrey Kunitsyn (andrey@traccar.org) * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,9 +16,20 @@ */ package org.traccar.api; -import java.io.IOException; -import java.sql.SQLException; +import com.google.inject.Provider; +import org.traccar.api.resource.SessionResource; +import org.traccar.api.security.PermissionsService; +import org.traccar.database.StatisticsManager; +import org.traccar.helper.Log; +import org.traccar.model.Device; +import org.traccar.storage.Storage; +import org.traccar.storage.StorageException; +import org.traccar.storage.query.Columns; +import org.traccar.storage.query.Condition; +import org.traccar.storage.query.Request; +import javax.inject.Inject; +import javax.inject.Singleton; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; @@ -28,16 +39,24 @@ import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; +import java.io.IOException; -import org.traccar.Context; -import org.traccar.Main; -import org.traccar.api.resource.SessionResource; -import org.traccar.database.StatisticsManager; -import org.traccar.helper.Log; -import org.traccar.model.Device; - +@Singleton public class MediaFilter implements Filter { + private final Storage storage; + private final StatisticsManager statisticsManager; + private final Provider<PermissionsService> permissionsServiceProvider; + + @Inject + public MediaFilter( + Storage storage, StatisticsManager statisticsManager, + Provider<PermissionsService> permissionsServiceProvider) { + this.storage = storage; + this.statisticsManager = statisticsManager; + this.permissionsServiceProvider = permissionsServiceProvider; + } + @Override public void init(FilterConfig filterConfig) throws ServletException { } @@ -45,6 +64,7 @@ public class MediaFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + HttpServletResponse httpResponse = (HttpServletResponse) response; try { HttpSession session = ((HttpServletRequest) request).getSession(false); @@ -52,8 +72,7 @@ public class MediaFilter implements Filter { if (session != null) { userId = (Long) session.getAttribute(SessionResource.USER_ID_KEY); if (userId != null) { - Context.getPermissionsManager().checkUserEnabled(userId); - Main.getInjector().getInstance(StatisticsManager.class).registerRequest(userId); + statisticsManager.registerRequest(userId); } } if (userId == null) { @@ -64,21 +83,19 @@ public class MediaFilter implements Filter { String path = ((HttpServletRequest) request).getPathInfo(); String[] parts = path != null ? path.split("/") : null; if (parts != null && parts.length >= 2) { - Device device = Context.getDeviceManager().getByUniqueId(parts[1]); + Device device = storage.getObject(Device.class, new Request( + new Columns.All(), new Condition.Equals("uniqueId", parts[1]))); if (device != null) { - Context.getPermissionsManager().checkDevice(userId, device.getId()); + permissionsServiceProvider.get().checkPermission(Device.class, userId, device.getId()); chain.doFilter(request, response); return; } } httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN); - } catch (SecurityException e) { + } catch (SecurityException | StorageException e) { httpResponse.setStatus(HttpServletResponse.SC_FORBIDDEN); httpResponse.getWriter().println(Log.exceptionStack(e)); - } catch (SQLException e) { - httpResponse.setStatus(HttpServletResponse.SC_BAD_REQUEST); - httpResponse.getWriter().println(Log.exceptionStack(e)); } } diff --git a/src/main/java/org/traccar/api/SimpleObjectResource.java b/src/main/java/org/traccar/api/SimpleObjectResource.java index a7fcae0e7..4a435ca7d 100644 --- a/src/main/java/org/traccar/api/SimpleObjectResource.java +++ b/src/main/java/org/traccar/api/SimpleObjectResource.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Anton Tananaev (anton@traccar.org) + * Copyright 2017 - 2022 Anton Tananaev (anton@traccar.org) * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org) * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,15 +16,17 @@ */ package org.traccar.api; -import java.sql.SQLException; -import java.util.Collection; +import org.traccar.model.BaseModel; +import org.traccar.model.User; +import org.traccar.storage.StorageException; +import org.traccar.storage.query.Columns; +import org.traccar.storage.query.Condition; +import org.traccar.storage.query.Request; import javax.ws.rs.GET; import javax.ws.rs.QueryParam; - -import org.traccar.Context; -import org.traccar.database.BaseObjectManager; -import org.traccar.model.BaseModel; +import java.util.Collection; +import java.util.LinkedList; public class SimpleObjectResource<T extends BaseModel> extends BaseObjectResource<T> { @@ -34,10 +36,24 @@ public class SimpleObjectResource<T extends BaseModel> extends BaseObjectResourc @GET public Collection<T> get( - @QueryParam("all") boolean all, @QueryParam("userId") long userId) throws SQLException { - - BaseObjectManager<T> manager = Context.getManager(getBaseClass()); - return manager.getItems(getSimpleManagerItems(manager, all, userId)); + @QueryParam("all") boolean all, @QueryParam("userId") long userId) throws StorageException { + + var conditions = new LinkedList<Condition>(); + + if (all) { + if (permissionsService.notAdmin(getUserId())) { + conditions.add(new Condition.Permission(User.class, getUserId(), baseClass)); + } + } else { + if (userId == 0) { + userId = getUserId(); + } else { + permissionsService.checkUser(getUserId(), userId); + } + conditions.add(new Condition.Permission(User.class, userId, baseClass)); + } + + return storage.getObjects(baseClass, new Request(new Columns.All(), Condition.merge(conditions))); } } diff --git a/src/main/java/org/traccar/api/resource/AttributeResource.java b/src/main/java/org/traccar/api/resource/AttributeResource.java index de69d871c..f85e90133 100644 --- a/src/main/java/org/traccar/api/resource/AttributeResource.java +++ b/src/main/java/org/traccar/api/resource/AttributeResource.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 - 2019 Anton Tananaev (anton@traccar.org) + * Copyright 2017 - 2022 Anton Tananaev (anton@traccar.org) * Copyright 2017 - 2018 Andrey Kunitsyn (andrey@traccar.org) * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,8 +16,7 @@ */ package org.traccar.api.resource; -import java.sql.SQLException; - +import javax.inject.Inject; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.POST; @@ -29,68 +28,72 @@ import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; -import org.traccar.Context; import org.traccar.api.ExtendedObjectResource; import org.traccar.model.Attribute; +import org.traccar.model.Device; import org.traccar.model.Position; import org.traccar.handler.ComputedAttributesHandler; +import org.traccar.storage.StorageException; +import org.traccar.storage.query.Columns; +import org.traccar.storage.query.Condition; +import org.traccar.storage.query.Request; @Path("attributes/computed") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public class AttributeResource extends ExtendedObjectResource<Attribute> { + @Inject + private ComputedAttributesHandler computedAttributesHandler; + public AttributeResource() { super(Attribute.class); } @POST @Path("test") - public Response test(@QueryParam("deviceId") long deviceId, Attribute entity) { - Context.getPermissionsManager().checkAdmin(getUserId()); - Context.getPermissionsManager().checkDevice(getUserId(), deviceId); - Position last = Context.getIdentityManager().getLastPosition(deviceId); - if (last != null) { - Object result = new ComputedAttributesHandler( - Context.getConfig(), - Context.getIdentityManager(), - Context.getAttributesManager()).computeAttribute(entity, last); - if (result != null) { - switch (entity.getType()) { - case "number": - Number numberValue = (Number) result; - return Response.ok(numberValue).build(); - case "boolean": - Boolean booleanValue = (Boolean) result; - return Response.ok(booleanValue).build(); - default: - return Response.ok(result.toString()).build(); - } - } else { - return Response.noContent().build(); + public Response test(@QueryParam("deviceId") long deviceId, Attribute entity) throws StorageException { + permissionsService.checkAdmin(getUserId()); + permissionsService.checkPermission(Device.class, getUserId(), deviceId); + + Position position = storage.getObject(Position.class, new Request( + new Columns.All(), + new Condition.LatestPositions(deviceId))); + + Object result = computedAttributesHandler.computeAttribute(entity, position); + if (result != null) { + switch (entity.getType()) { + case "number": + Number numberValue = (Number) result; + return Response.ok(numberValue).build(); + case "boolean": + Boolean booleanValue = (Boolean) result; + return Response.ok(booleanValue).build(); + default: + return Response.ok(result.toString()).build(); } } else { - throw new IllegalArgumentException("Device has no last position"); + return Response.noContent().build(); } } @POST - public Response add(Attribute entity) throws SQLException { - Context.getPermissionsManager().checkAdmin(getUserId()); + public Response add(Attribute entity) throws StorageException { + permissionsService.checkAdmin(getUserId()); return super.add(entity); } @Path("{id}") @PUT - public Response update(Attribute entity) throws SQLException { - Context.getPermissionsManager().checkAdmin(getUserId()); + public Response update(Attribute entity) throws StorageException { + permissionsService.checkAdmin(getUserId()); return super.update(entity); } @Path("{id}") @DELETE - public Response remove(@PathParam("id") long id) throws SQLException { - Context.getPermissionsManager().checkAdmin(getUserId()); + public Response remove(@PathParam("id") long id) throws StorageException { + permissionsService.checkAdmin(getUserId()); return super.remove(id); } diff --git a/src/main/java/org/traccar/api/resource/CommandResource.java b/src/main/java/org/traccar/api/resource/CommandResource.java index a31345246..6ef6ee9c5 100644 --- a/src/main/java/org/traccar/api/resource/CommandResource.java +++ b/src/main/java/org/traccar/api/resource/CommandResource.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 - 2019 Anton Tananaev (anton@traccar.org) + * Copyright 2015 - 2022 Anton Tananaev (anton@traccar.org) * Copyright 2016 Gabor Somogyi (gabor.g.somogyi@gmail.com) * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org) * @@ -17,16 +17,24 @@ */ package org.traccar.api.resource; -import org.traccar.Context; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.traccar.BaseProtocol; +import org.traccar.ServerManager; import org.traccar.api.ExtendedObjectResource; import org.traccar.database.CommandsManager; import org.traccar.model.Command; +import org.traccar.model.Device; +import org.traccar.model.Position; import org.traccar.model.Typed; +import org.traccar.model.User; +import org.traccar.model.UserRestrictions; +import org.traccar.storage.StorageException; +import org.traccar.storage.query.Columns; +import org.traccar.storage.query.Condition; +import org.traccar.storage.query.Request; -import java.util.Collection; -import java.util.HashSet; -import java.util.Set; - +import javax.inject.Inject; import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.POST; @@ -35,40 +43,80 @@ import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; @Path("commands") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public class CommandResource extends ExtendedObjectResource<Command> { + private static final Logger LOGGER = LoggerFactory.getLogger(CommandResource.class); + + @Inject + private CommandsManager commandsManager; + + @Inject + private ServerManager serverManager; + public CommandResource() { super(Command.class); } + private BaseProtocol getDeviceProtocol(long deviceId) throws StorageException { + Position position = storage.getObject(Position.class, new Request( + new Columns.All(), new Condition.LatestPositions(deviceId))); + if (position != null) { + return serverManager.getProtocol(position.getProtocol()); + } else { + return null; + } + } + @GET @Path("send") - public Collection<Command> get(@QueryParam("deviceId") long deviceId) { - Context.getPermissionsManager().checkDevice(getUserId(), deviceId); - CommandsManager commandsManager = Context.getCommandsManager(); - Set<Long> result = new HashSet<>(commandsManager.getUserItems(getUserId())); - result.retainAll(commandsManager.getSupportedCommands(deviceId)); - return commandsManager.getItems(result); + public Collection<Command> get(@QueryParam("deviceId") long deviceId) throws StorageException { + permissionsService.checkPermission(Device.class, getUserId(), deviceId); + BaseProtocol protocol = getDeviceProtocol(deviceId); + + var commands = storage.getObjects(baseClass, new Request( + new Columns.All(), + Condition.merge(List.of( + new Condition.Permission(User.class, getUserId(), baseClass), + new Condition.Permission(Device.class, deviceId, baseClass) + )))); + + return commands.stream().filter(command -> { + String type = command.getType(); + if (protocol != null) { + return command.getTextChannel() && protocol.getSupportedTextCommands().contains(type) + || !command.getTextChannel() && protocol.getSupportedDataCommands().contains(type); + } else { + return type.equals(Command.TYPE_CUSTOM); + } + }).collect(Collectors.toList()); } @POST @Path("send") public Response send(Command entity) throws Exception { - Context.getPermissionsManager().checkReadonly(getUserId()); - long deviceId = entity.getDeviceId(); - long id = entity.getId(); - Context.getPermissionsManager().checkDevice(getUserId(), deviceId); - if (id != 0) { - Context.getPermissionsManager().checkPermission(Command.class, getUserId(), id); - Context.getPermissionsManager().checkUserDeviceCommand(getUserId(), deviceId, id); + permissionsService.checkRestriction(getUserId(), UserRestrictions::getReadonly); + if (entity.getId() > 0) { + permissionsService.checkPermission(baseClass, getUserId(), entity.getId()); + long deviceId = entity.getDeviceId(); + entity = storage.getObject(baseClass, new Request( + new Columns.All(), new Condition.Equals("id", entity.getId()))); + entity.setDeviceId(deviceId); } else { - Context.getPermissionsManager().checkLimitCommands(getUserId()); + permissionsService.checkRestriction(getUserId(), UserRestrictions::getLimitCommands); } - if (!Context.getCommandsManager().sendCommand(entity)) { + permissionsService.checkPermission(Device.class, getUserId(), entity.getDeviceId()); + if (!commandsManager.sendCommand(entity)) { return Response.accepted(entity).build(); } return Response.ok(entity).build(); @@ -78,15 +126,33 @@ public class CommandResource extends ExtendedObjectResource<Command> { @Path("types") public Collection<Typed> get( @QueryParam("deviceId") long deviceId, - @QueryParam("protocol") String protocol, - @QueryParam("textChannel") boolean textChannel) { + @QueryParam("textChannel") boolean textChannel) throws StorageException { if (deviceId != 0) { - Context.getPermissionsManager().checkDevice(getUserId(), deviceId); - return Context.getCommandsManager().getCommandTypes(deviceId, textChannel); - } else if (protocol != null) { - return Context.getCommandsManager().getCommandTypes(protocol, textChannel); + permissionsService.checkPermission(Device.class, getUserId(), deviceId); + BaseProtocol protocol = getDeviceProtocol(deviceId); + if (protocol != null) { + if (textChannel) { + return protocol.getSupportedTextCommands().stream().map(Typed::new).collect(Collectors.toList()); + } else { + return protocol.getSupportedDataCommands().stream().map(Typed::new).collect(Collectors.toList()); + } + } else { + return Collections.singletonList(new Typed(Command.TYPE_CUSTOM)); + } } else { - return Context.getCommandsManager().getAllCommandTypes(); + List<Typed> result = new ArrayList<>(); + Field[] fields = Command.class.getDeclaredFields(); + for (Field field : fields) { + if (Modifier.isStatic(field.getModifiers()) && field.getName().startsWith("TYPE_")) { + try { + result.add(new Typed(field.get(null).toString())); + } catch (IllegalArgumentException | IllegalAccessException error) { + LOGGER.warn("Get command types error", error); + } + } + } + return result; } } + } diff --git a/src/main/java/org/traccar/api/resource/DeviceResource.java b/src/main/java/org/traccar/api/resource/DeviceResource.java index 7006cdb84..c0b0cea0d 100644 --- a/src/main/java/org/traccar/api/resource/DeviceResource.java +++ b/src/main/java/org/traccar/api/resource/DeviceResource.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org) + * Copyright 2015 - 2022 Anton Tananaev (anton@traccar.org) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,33 +15,58 @@ */ package org.traccar.api.resource; -import org.traccar.Context; import org.traccar.api.BaseObjectResource; -import org.traccar.database.DeviceManager; +import org.traccar.broadcast.BroadcastService; +import org.traccar.database.MediaManager; import org.traccar.helper.LogAction; import org.traccar.model.Device; import org.traccar.model.DeviceAccumulators; +import org.traccar.model.Position; +import org.traccar.model.User; +import org.traccar.session.ConnectionManager; +import org.traccar.session.cache.CacheManager; +import org.traccar.storage.StorageException; +import org.traccar.storage.query.Columns; +import org.traccar.storage.query.Condition; +import org.traccar.storage.query.Request; +import javax.inject.Inject; import javax.ws.rs.Consumes; import javax.ws.rs.GET; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.POST; import javax.ws.rs.PUT; import javax.ws.rs.Path; +import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; +import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; - -import java.sql.SQLException; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; import java.util.Collection; -import java.util.HashSet; +import java.util.LinkedList; import java.util.List; -import java.util.Set; @Path("devices") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public class DeviceResource extends BaseObjectResource<Device> { + @Inject + private CacheManager cacheManager; + + @Inject + private ConnectionManager connectionManager; + + @Inject + private BroadcastService broadcastService; + + @Inject + private MediaManager mediaManager; + public DeviceResource() { super(Device.class); } @@ -50,51 +75,112 @@ public class DeviceResource extends BaseObjectResource<Device> { public Collection<Device> get( @QueryParam("all") boolean all, @QueryParam("userId") long userId, @QueryParam("uniqueId") List<String> uniqueIds, - @QueryParam("id") List<Long> deviceIds) throws SQLException { - DeviceManager deviceManager = Context.getDeviceManager(); - Set<Long> result; - if (all) { - if (Context.getPermissionsManager().getUserAdmin(getUserId())) { - result = deviceManager.getAllItems(); - } else { - Context.getPermissionsManager().checkManager(getUserId()); - result = deviceManager.getManagedItems(getUserId()); - } - } else if (uniqueIds.isEmpty() && deviceIds.isEmpty()) { - if (userId == 0) { - userId = getUserId(); - } - Context.getPermissionsManager().checkUser(getUserId(), userId); - if (Context.getPermissionsManager().getUserAdmin(getUserId())) { - result = deviceManager.getAllUserItems(userId); - } else { - result = deviceManager.getUserItems(userId); - } - } else { - result = new HashSet<>(); + @QueryParam("id") List<Long> deviceIds) throws StorageException { + + if (!uniqueIds.isEmpty() || !deviceIds.isEmpty()) { + + List<Device> result = new LinkedList<>(); for (String uniqueId : uniqueIds) { - Device device = deviceManager.getByUniqueId(uniqueId); - Context.getPermissionsManager().checkDevice(getUserId(), device.getId()); - result.add(device.getId()); + result.addAll(storage.getObjects(Device.class, new Request( + new Columns.All(), + new Condition.And( + new Condition.Equals("uniqueId", uniqueId), + new Condition.Permission(User.class, getUserId(), Device.class))))); } for (Long deviceId : deviceIds) { - Context.getPermissionsManager().checkDevice(getUserId(), deviceId); - result.add(deviceId); + result.addAll(storage.getObjects(Device.class, new Request( + new Columns.All(), + new Condition.And( + new Condition.Equals("id", deviceId), + new Condition.Permission(User.class, getUserId(), Device.class))))); } + return result; + + } else { + + var conditions = new LinkedList<Condition>(); + + if (all) { + if (permissionsService.notAdmin(getUserId())) { + conditions.add(new Condition.Permission(User.class, getUserId(), baseClass)); + } + } else { + if (userId == 0) { + conditions.add(new Condition.Permission(User.class, getUserId(), baseClass)); + } else { + permissionsService.checkUser(getUserId(), userId); + conditions.add(new Condition.Permission(User.class, userId, baseClass).excludeGroups()); + } + } + + return storage.getObjects(baseClass, new Request(new Columns.All(), Condition.merge(conditions))); + } - return deviceManager.getItems(result); } @Path("{id}/accumulators") @PUT - public Response updateAccumulators(DeviceAccumulators entity) throws SQLException { - if (!Context.getPermissionsManager().getUserAdmin(getUserId())) { - Context.getPermissionsManager().checkManager(getUserId()); - Context.getPermissionsManager().checkPermission(Device.class, getUserId(), entity.getDeviceId()); + public Response updateAccumulators(DeviceAccumulators entity) throws StorageException { + if (permissionsService.notAdmin(getUserId())) { + permissionsService.checkManager(getUserId()); + permissionsService.checkPermission(Device.class, getUserId(), entity.getDeviceId()); } - Context.getDeviceManager().resetDeviceAccumulators(entity); + + Position position = storage.getObject(Position.class, new Request( + new Columns.All(), new Condition.LatestPositions(entity.getDeviceId()))); + if (position != null) { + if (entity.getTotalDistance() != null) { + position.getAttributes().put(Position.KEY_TOTAL_DISTANCE, entity.getTotalDistance()); + } + if (entity.getHours() != null) { + position.getAttributes().put(Position.KEY_HOURS, entity.getHours()); + } + position.setId(storage.addObject(position, new Request(new Columns.Exclude("id")))); + + Device device = new Device(); + device.setId(position.getDeviceId()); + device.setPositionId(position.getId()); + storage.updateObject(device, new Request( + new Columns.Include("positionId"), + new Condition.Equals("id", device.getId()))); + + try { + cacheManager.addDevice(position.getDeviceId()); + cacheManager.updatePosition(position); + connectionManager.updatePosition(true, position); + } finally { + cacheManager.removeDevice(position.getDeviceId()); + } + } else { + throw new IllegalArgumentException(); + } + LogAction.resetDeviceAccumulators(getUserId(), entity.getDeviceId()); return Response.noContent().build(); } + @Path("{id}/image") + @POST + @Consumes("image/*") + public Response uploadImage( + @PathParam("id") long deviceId, File file, + @HeaderParam(HttpHeaders.CONTENT_TYPE) String type) throws StorageException, IOException { + + Device device = storage.getObject(Device.class, new Request( + new Columns.All(), + new Condition.And( + new Condition.Equals("id", deviceId), + new Condition.Permission(User.class, getUserId(), Device.class)))); + if (device != null) { + String name = "device"; + String extension = type.substring("image/".length()); + try (var input = new FileInputStream(file); + var output = mediaManager.createFileStream(device.getUniqueId(), name, extension)) { + input.transferTo(output); + } + return Response.ok(name + "." + extension).build(); + } + return Response.status(Response.Status.NOT_FOUND).build(); + } + } diff --git a/src/main/java/org/traccar/api/resource/EventResource.java b/src/main/java/org/traccar/api/resource/EventResource.java index 34e4a94ce..afdaf52b5 100644 --- a/src/main/java/org/traccar/api/resource/EventResource.java +++ b/src/main/java/org/traccar/api/resource/EventResource.java @@ -15,7 +15,13 @@ */ package org.traccar.api.resource; -import java.sql.SQLException; +import org.traccar.api.BaseResource; +import org.traccar.model.Device; +import org.traccar.model.Event; +import org.traccar.storage.StorageException; +import org.traccar.storage.query.Columns; +import org.traccar.storage.query.Condition; +import org.traccar.storage.query.Request; import javax.ws.rs.Consumes; import javax.ws.rs.GET; @@ -26,32 +32,20 @@ import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; -import org.traccar.Context; -import org.traccar.api.BaseResource; -import org.traccar.model.Event; -import org.traccar.model.Geofence; -import org.traccar.model.Maintenance; - @Path("events") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) - public class EventResource extends BaseResource { @Path("{id}") @GET - public Event get(@PathParam("id") long id) throws SQLException { - Event event = Context.getDataManager().getObject(Event.class, id); + public Event get(@PathParam("id") long id) throws StorageException { + Event event = storage.getObject(Event.class, new Request( + new Columns.All(), new Condition.Equals("id", id))); if (event == null) { throw new WebApplicationException(Response.status(Response.Status.NOT_FOUND).build()); } - Context.getPermissionsManager().checkDevice(getUserId(), event.getDeviceId()); - if (event.getGeofenceId() != 0) { - Context.getPermissionsManager().checkPermission(Geofence.class, getUserId(), event.getGeofenceId()); - } - if (event.getMaintenanceId() != 0) { - Context.getPermissionsManager().checkPermission(Maintenance.class, getUserId(), event.getMaintenanceId()); - } + permissionsService.checkPermission(Device.class, getUserId(), event.getDeviceId()); return event; } diff --git a/src/main/java/org/traccar/api/resource/NotificationResource.java b/src/main/java/org/traccar/api/resource/NotificationResource.java index 9631a52b7..2e4ad12f3 100644 --- a/src/main/java/org/traccar/api/resource/NotificationResource.java +++ b/src/main/java/org/traccar/api/resource/NotificationResource.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org) + * Copyright 2016 - 2022 Anton Tananaev (anton@traccar.org) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,8 +15,18 @@ */ package org.traccar.api.resource; -import java.util.Collection; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.traccar.api.ExtendedObjectResource; +import org.traccar.model.Event; +import org.traccar.model.Notification; +import org.traccar.model.Typed; +import org.traccar.model.User; +import org.traccar.notification.MessageException; +import org.traccar.notification.NotificatorManager; +import org.traccar.storage.StorageException; +import javax.inject.Inject; import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.POST; @@ -25,20 +35,22 @@ import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; - -import org.traccar.Context; -import org.traccar.api.ExtendedObjectResource; -import org.traccar.model.Event; -import org.traccar.model.Notification; -import org.traccar.model.Typed; -import org.traccar.notification.MessageException; - +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; @Path("notifications") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public class NotificationResource extends ExtendedObjectResource<Notification> { + private static final Logger LOGGER = LoggerFactory.getLogger(NotificationResource.class); + + @Inject + private NotificatorManager notificatorManager; + public NotificationResource() { super(Notification.class); } @@ -46,21 +58,32 @@ public class NotificationResource extends ExtendedObjectResource<Notification> { @GET @Path("types") public Collection<Typed> get() { - return Context.getNotificationManager().getAllNotificationTypes(); + List<Typed> types = new LinkedList<>(); + Field[] fields = Event.class.getDeclaredFields(); + for (Field field : fields) { + if (Modifier.isStatic(field.getModifiers()) && field.getName().startsWith("TYPE_")) { + try { + types.add(new Typed(field.get(null).toString())); + } catch (IllegalArgumentException | IllegalAccessException error) { + LOGGER.warn("Get event types error", error); + } + } + } + return types; } @GET @Path("notificators") public Collection<Typed> getNotificators() { - return Context.getNotificatorManager().getAllNotificatorTypes(); + return notificatorManager.getAllNotificatorTypes(); } @POST @Path("test") - public Response testMessage() throws MessageException, InterruptedException { - for (Typed method : Context.getNotificatorManager().getAllNotificatorTypes()) { - Context.getNotificatorManager() - .getNotificator(method.getType()).sendSync(getUserId(), new Event("test", 0), null); + public Response testMessage() throws MessageException, InterruptedException, StorageException { + User user = permissionsService.getUser(getUserId()); + for (Typed method : notificatorManager.getAllNotificatorTypes()) { + notificatorManager.getNotificator(method.getType()).send(user, new Event("test", 0), null); } return Response.noContent().build(); } @@ -68,8 +91,9 @@ public class NotificationResource extends ExtendedObjectResource<Notification> { @POST @Path("test/{notificator}") public Response testMessage(@PathParam("notificator") String notificator) - throws MessageException, InterruptedException { - Context.getNotificatorManager().getNotificator(notificator).sendSync(getUserId(), new Event("test", 0), null); + throws MessageException, InterruptedException, StorageException { + User user = permissionsService.getUser(getUserId()); + notificatorManager.getNotificator(notificator).send(user, new Event("test", 0), null); return Response.noContent().build(); } diff --git a/src/main/java/org/traccar/api/resource/PasswordResource.java b/src/main/java/org/traccar/api/resource/PasswordResource.java index 1868a6191..2d87a8665 100644 --- a/src/main/java/org/traccar/api/resource/PasswordResource.java +++ b/src/main/java/org/traccar/api/resource/PasswordResource.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 Anton Tananaev (anton@traccar.org) + * Copyright 2021 - 2022 Anton Tananaev (anton@traccar.org) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,14 +15,18 @@ */ package org.traccar.api.resource; -import org.apache.velocity.VelocityContext; -import org.traccar.Context; import org.traccar.api.BaseResource; +import org.traccar.api.signature.TokenManager; +import org.traccar.mail.MailManager; import org.traccar.model.User; -import org.traccar.notification.NotificationMessage; import org.traccar.notification.TextTemplateFormatter; +import org.traccar.storage.StorageException; +import org.traccar.storage.query.Columns; +import org.traccar.storage.query.Condition; +import org.traccar.storage.query.Request; import javax.annotation.security.PermitAll; +import javax.inject.Inject; import javax.mail.MessagingException; import javax.ws.rs.Consumes; import javax.ws.rs.FormParam; @@ -31,33 +35,35 @@ import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; -import java.sql.SQLException; -import java.util.UUID; +import java.io.IOException; +import java.security.GeneralSecurityException; @Path("password") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_FORM_URLENCODED) public class PasswordResource extends BaseResource { - private static final String PASSWORD_RESET_TOKEN = "passwordToken"; + @Inject + private MailManager mailManager; + + @Inject + private TokenManager tokenManager; + + @Inject + private TextTemplateFormatter textTemplateFormatter; @Path("reset") @PermitAll @POST - public Response reset(@FormParam("email") String email) throws SQLException, MessagingException { - for (long userId : Context.getUsersManager().getAllItems()) { - User user = Context.getUsersManager().getById(userId); - if (email.equals(user.getEmail())) { - String token = UUID.randomUUID().toString().replaceAll("-", ""); - user.set(PASSWORD_RESET_TOKEN, token); - Context.getUsersManager().updateItem(user); - VelocityContext velocityContext = TextTemplateFormatter.prepareContext(null); - velocityContext.put("token", token); - NotificationMessage fullMessage = - TextTemplateFormatter.formatMessage(velocityContext, "passwordReset", "full"); - Context.getMailManager().sendMessage(userId, fullMessage.getSubject(), fullMessage.getBody()); - break; - } + public Response reset(@FormParam("email") String email) + throws StorageException, MessagingException, GeneralSecurityException, IOException { + + User user = storage.getObject(User.class, new Request( + new Columns.All(), new Condition.Equals("email", email))); + if (user != null) { + var velocityContext = textTemplateFormatter.prepareContext(permissionsService.getServer(), user); + var fullMessage = textTemplateFormatter.formatMessage(velocityContext, "passwordReset", "full"); + mailManager.sendMessage(user, fullMessage.getSubject(), fullMessage.getBody()); } return Response.ok().build(); } @@ -66,15 +72,18 @@ public class PasswordResource extends BaseResource { @PermitAll @POST public Response update( - @FormParam("token") String token, @FormParam("password") String password) throws SQLException { - for (long userId : Context.getUsersManager().getAllItems()) { - User user = Context.getUsersManager().getById(userId); - if (token.equals(user.getString(PASSWORD_RESET_TOKEN))) { - user.getAttributes().remove(PASSWORD_RESET_TOKEN); - user.setPassword(password); - Context.getUsersManager().updateItem(user); - return Response.ok().build(); - } + @FormParam("token") String token, @FormParam("password") String password) + throws StorageException, GeneralSecurityException, IOException { + + long userId = tokenManager.verifyToken(token); + User user = storage.getObject(User.class, new Request( + new Columns.All(), new Condition.Equals("id", userId))); + if (user != null) { + user.setPassword(password); + storage.updateObject(user, new Request( + new Columns.Include("hashedPassword", "salt"), + new Condition.Equals("id", userId))); + return Response.ok().build(); } return Response.status(Response.Status.NOT_FOUND).build(); } diff --git a/src/main/java/org/traccar/api/resource/PermissionsResource.java b/src/main/java/org/traccar/api/resource/PermissionsResource.java index 54d3964b6..d35cb98bb 100644 --- a/src/main/java/org/traccar/api/resource/PermissionsResource.java +++ b/src/main/java/org/traccar/api/resource/PermissionsResource.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Anton Tananaev (anton@traccar.org) + * Copyright 2017 - 2022 Anton Tananaev (anton@traccar.org) * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org) * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,12 +16,14 @@ */ package org.traccar.api.resource; -import java.sql.SQLException; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Set; +import org.traccar.api.BaseResource; +import org.traccar.helper.LogAction; +import org.traccar.model.Permission; +import org.traccar.model.UserRestrictions; +import org.traccar.session.cache.CacheManager; +import org.traccar.storage.StorageException; +import javax.inject.Inject; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.POST; @@ -30,33 +32,24 @@ import javax.ws.rs.Produces; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; - -import org.traccar.Context; -import org.traccar.api.BaseResource; -import org.traccar.helper.LogAction; -import org.traccar.model.Device; -import org.traccar.model.Permission; -import org.traccar.model.User; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Set; @Path("permissions") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public class PermissionsResource extends BaseResource { - private void checkPermission(Permission permission, boolean link) { - if (!link && permission.getOwnerClass().equals(User.class) - && permission.getPropertyClass().equals(Device.class)) { - if (getUserId() != permission.getOwnerId()) { - Context.getPermissionsManager().checkUser(getUserId(), permission.getOwnerId()); - } else { - Context.getPermissionsManager().checkAdmin(getUserId()); - } - } else { - Context.getPermissionsManager().checkPermission( - permission.getOwnerClass(), getUserId(), permission.getOwnerId()); + @Inject + private CacheManager cacheManager; + + private void checkPermission(Permission permission) throws StorageException { + if (permissionsService.notAdmin(getUserId())) { + permissionsService.checkPermission(permission.getOwnerClass(), getUserId(), permission.getOwnerId()); + permissionsService.checkPermission(permission.getOwnerClass(), getUserId(), permission.getOwnerId()); } - Context.getPermissionsManager().checkPermission( - permission.getPropertyClass(), getUserId(), permission.getPropertyId()); } private void checkPermissionTypes(List<LinkedHashMap<String, Long>> entities) { @@ -71,49 +64,51 @@ public class PermissionsResource extends BaseResource { @Path("bulk") @POST - public Response add(List<LinkedHashMap<String, Long>> entities) throws SQLException, ClassNotFoundException { - Context.getPermissionsManager().checkReadonly(getUserId()); + public Response add(List<LinkedHashMap<String, Long>> entities) throws StorageException, ClassNotFoundException { + permissionsService.checkRestriction(getUserId(), UserRestrictions::getReadonly); checkPermissionTypes(entities); for (LinkedHashMap<String, Long> entity: entities) { Permission permission = new Permission(entity); - checkPermission(permission, true); - Context.getDataManager().linkObject(permission.getOwnerClass(), permission.getOwnerId(), - permission.getPropertyClass(), permission.getPropertyId(), true); - LogAction.link(getUserId(), permission.getOwnerClass(), permission.getOwnerId(), + checkPermission(permission); + storage.addPermission(permission); + cacheManager.invalidatePermission( + true, + permission.getOwnerClass(), permission.getOwnerId(), + permission.getPropertyClass(), permission.getPropertyId()); + LogAction.link(getUserId(), + permission.getOwnerClass(), permission.getOwnerId(), permission.getPropertyClass(), permission.getPropertyId()); - } - if (!entities.isEmpty()) { - Context.getPermissionsManager().refreshPermissions(new Permission(entities.get(0))); } return Response.noContent().build(); } @POST - public Response add(LinkedHashMap<String, Long> entity) throws SQLException, ClassNotFoundException { + public Response add(LinkedHashMap<String, Long> entity) throws StorageException, ClassNotFoundException { return add(Collections.singletonList(entity)); } @DELETE @Path("bulk") - public Response remove(List<LinkedHashMap<String, Long>> entities) throws SQLException, ClassNotFoundException { - Context.getPermissionsManager().checkReadonly(getUserId()); + public Response remove(List<LinkedHashMap<String, Long>> entities) throws StorageException, ClassNotFoundException { + permissionsService.checkRestriction(getUserId(), UserRestrictions::getReadonly); checkPermissionTypes(entities); for (LinkedHashMap<String, Long> entity: entities) { Permission permission = new Permission(entity); - checkPermission(permission, false); - Context.getDataManager().linkObject(permission.getOwnerClass(), permission.getOwnerId(), - permission.getPropertyClass(), permission.getPropertyId(), false); - LogAction.unlink(getUserId(), permission.getOwnerClass(), permission.getOwnerId(), + checkPermission(permission); + storage.removePermission(permission); + cacheManager.invalidatePermission( + true, + permission.getOwnerClass(), permission.getOwnerId(), + permission.getPropertyClass(), permission.getPropertyId()); + LogAction.unlink(getUserId(), + permission.getOwnerClass(), permission.getOwnerId(), permission.getPropertyClass(), permission.getPropertyId()); - } - if (!entities.isEmpty()) { - Context.getPermissionsManager().refreshPermissions(new Permission(entities.get(0))); } return Response.noContent().build(); } @DELETE - public Response remove(LinkedHashMap<String, Long> entity) throws SQLException, ClassNotFoundException { + public Response remove(LinkedHashMap<String, Long> entity) throws StorageException, ClassNotFoundException { return remove(Collections.singletonList(entity)); } diff --git a/src/main/java/org/traccar/api/resource/PositionResource.java b/src/main/java/org/traccar/api/resource/PositionResource.java index 998d59706..042dd1e23 100644 --- a/src/main/java/org/traccar/api/resource/PositionResource.java +++ b/src/main/java/org/traccar/api/resource/PositionResource.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 - 2020 Anton Tananaev (anton@traccar.org) + * Copyright 2015 - 2022 Anton Tananaev (anton@traccar.org) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,20 +15,32 @@ */ package org.traccar.api.resource; -import org.traccar.Context; import org.traccar.api.BaseResource; +import org.traccar.helper.model.PositionUtil; +import org.traccar.model.Device; import org.traccar.model.Position; +import org.traccar.model.UserRestrictions; +import org.traccar.reports.CsvExportProvider; +import org.traccar.reports.GpxExportProvider; +import org.traccar.reports.KmlExportProvider; +import org.traccar.storage.StorageException; +import org.traccar.storage.query.Columns; +import org.traccar.storage.query.Condition; +import org.traccar.storage.query.Request; +import javax.inject.Inject; import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; -import java.sql.SQLException; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.StreamingOutput; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.Date; import java.util.List; @@ -37,29 +49,95 @@ import java.util.List; @Consumes(MediaType.APPLICATION_JSON) public class PositionResource extends BaseResource { + @Inject + private KmlExportProvider kmlExportProvider; + + @Inject + private CsvExportProvider csvExportProvider; + + @Inject + private GpxExportProvider gpxExportProvider; + @GET public Collection<Position> getJson( @QueryParam("deviceId") long deviceId, @QueryParam("id") List<Long> positionIds, @QueryParam("from") Date from, @QueryParam("to") Date to) - throws SQLException { + throws StorageException { if (!positionIds.isEmpty()) { - ArrayList<Position> positions = new ArrayList<>(); - for (Long positionId : positionIds) { - Position position = Context.getDataManager().getObject(Position.class, positionId); - Context.getPermissionsManager().checkDevice(getUserId(), position.getDeviceId()); + var positions = new ArrayList<Position>(); + for (long positionId : positionIds) { + Position position = storage.getObject(Position.class, new Request( + new Columns.All(), new Condition.Equals("id", positionId))); + permissionsService.checkPermission(Device.class, getUserId(), position.getDeviceId()); positions.add(position); } return positions; - } else if (deviceId == 0) { - return Context.getDeviceManager().getInitialState(getUserId()); - } else { - Context.getPermissionsManager().checkDevice(getUserId(), deviceId); + } else if (deviceId > 0) { + permissionsService.checkPermission(Device.class, getUserId(), deviceId); if (from != null && to != null) { - return Context.getDataManager().getPositions(deviceId, from, to); + permissionsService.checkRestriction(getUserId(), UserRestrictions::getDisableReports); + return PositionUtil.getPositions(storage, deviceId, from, to); } else { - return Collections.singleton(Context.getDeviceManager().getLastPosition(deviceId)); + return storage.getObjects(Position.class, new Request( + new Columns.All(), new Condition.LatestPositions(deviceId))); } + } else { + return PositionUtil.getLatestPositions(storage, getUserId()); } } + @Path("kml") + @GET + @Produces("application/vnd.google-earth.kml+xml") + public Response getKml( + @QueryParam("deviceId") long deviceId, + @QueryParam("from") Date from, @QueryParam("to") Date to) throws StorageException { + permissionsService.checkPermission(Device.class, getUserId(), deviceId); + StreamingOutput stream = output -> { + try { + kmlExportProvider.generate(output, deviceId, from, to); + } catch (StorageException e) { + throw new WebApplicationException(e); + } + }; + return Response.ok(stream) + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=positions.kml").build(); + } + + @Path("csv") + @GET + @Produces("text/csv") + public Response getCsv( + @QueryParam("deviceId") long deviceId, + @QueryParam("from") Date from, @QueryParam("to") Date to) throws StorageException { + permissionsService.checkPermission(Device.class, getUserId(), deviceId); + StreamingOutput stream = output -> { + try { + csvExportProvider.generate(output, deviceId, from, to); + } catch (StorageException e) { + throw new WebApplicationException(e); + } + }; + return Response.ok(stream) + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=positions.csv").build(); + } + + @Path("gpx") + @GET + @Produces("application/gpx+xml") + public Response getGpx( + @QueryParam("deviceId") long deviceId, + @QueryParam("from") Date from, @QueryParam("to") Date to) throws StorageException { + permissionsService.checkPermission(Device.class, getUserId(), deviceId); + StreamingOutput stream = output -> { + try { + gpxExportProvider.generate(output, deviceId, from, to); + } catch (StorageException e) { + throw new WebApplicationException(e); + } + }; + return Response.ok(stream) + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=positions.gpx").build(); + } + } diff --git a/src/main/java/org/traccar/api/resource/ReportResource.java b/src/main/java/org/traccar/api/resource/ReportResource.java index 7347bfd64..70177dd4d 100644 --- a/src/main/java/org/traccar/api/resource/ReportResource.java +++ b/src/main/java/org/traccar/api/resource/ReportResource.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 - 2020 Anton Tananaev (anton@traccar.org) + * Copyright 2016 - 2022 Anton Tananaev (anton@traccar.org) * Copyright 2016 - 2018 Andrey Kunitsyn (andrey@traccar.org) * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,41 +16,47 @@ */ package org.traccar.api.resource; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.sql.SQLException; -import java.util.Collection; -import java.util.Date; -import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.traccar.api.BaseResource; +import org.traccar.mail.MailManager; +import org.traccar.helper.LogAction; +import org.traccar.model.Event; +import org.traccar.model.Position; +import org.traccar.model.User; +import org.traccar.model.UserRestrictions; +import org.traccar.reports.EventsReportProvider; +import org.traccar.reports.RouteReportProvider; +import org.traccar.reports.StopsReportProvider; +import org.traccar.reports.SummaryReportProvider; +import org.traccar.reports.TripsReportProvider; +import org.traccar.reports.model.StopReportItem; +import org.traccar.reports.model.SummaryReportItem; +import org.traccar.reports.model.TripReportItem; +import org.traccar.storage.StorageException; import javax.activation.DataHandler; +import javax.inject.Inject; import javax.mail.MessagingException; import javax.mail.internet.MimeBodyPart; import javax.mail.util.ByteArrayDataSource; import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.Path; +import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; +import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.traccar.Context; -import org.traccar.api.BaseResource; -import org.traccar.helper.LogAction; -import org.traccar.model.Event; -import org.traccar.model.Position; -import org.traccar.reports.Events; -import org.traccar.reports.Summary; -import org.traccar.reports.Trips; -import org.traccar.reports.model.StopReport; -import org.traccar.reports.model.SummaryReport; -import org.traccar.reports.model.TripReport; -import org.traccar.reports.Route; -import org.traccar.reports.Stops; +import javax.ws.rs.core.StreamingOutput; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Collection; +import java.util.Date; +import java.util.List; @Path("reports") @Produces(MediaType.APPLICATION_JSON) @@ -59,155 +65,267 @@ public class ReportResource extends BaseResource { private static final Logger LOGGER = LoggerFactory.getLogger(ReportResource.class); - private static final String XLSX = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; - private static final String CONTENT_DISPOSITION_VALUE_XLSX = "attachment; filename=report.xlsx"; + private static final String EXCEL = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; + + @Inject + private EventsReportProvider eventsReportProvider; + + @Inject + private RouteReportProvider routeReportProvider; + + @Inject + private StopsReportProvider stopsReportProvider; + + @Inject + private SummaryReportProvider summaryReportProvider; + + @Inject + private TripsReportProvider tripsReportProvider; + + @Inject + private MailManager mailManager; private interface ReportExecutor { - void execute(ByteArrayOutputStream stream) throws SQLException, IOException; + void execute(OutputStream stream) throws StorageException, IOException; } private Response executeReport( - long userId, boolean mail, ReportExecutor executor) throws SQLException, IOException { - final ByteArrayOutputStream stream = new ByteArrayOutputStream(); + long userId, boolean mail, ReportExecutor executor) { if (mail) { new Thread(() -> { try { + var stream = new ByteArrayOutputStream(); executor.execute(stream); MimeBodyPart attachment = new MimeBodyPart(); - attachment.setFileName("report.xlsx"); attachment.setDataHandler(new DataHandler(new ByteArrayDataSource( stream.toByteArray(), "application/octet-stream"))); - Context.getMailManager().sendMessage( - userId, "Report", "The report is in the attachment.", attachment); - } catch (SQLException | IOException | MessagingException e) { + User user = permissionsService.getUser(userId); + mailManager.sendMessage(user, "Report", "The report is in the attachment.", attachment); + } catch (StorageException | IOException | MessagingException e) { LOGGER.warn("Report failed", e); } }).start(); return Response.noContent().build(); } else { - executor.execute(stream); - return Response.ok(stream.toByteArray()) - .header(HttpHeaders.CONTENT_DISPOSITION, CONTENT_DISPOSITION_VALUE_XLSX).build(); + StreamingOutput stream = output -> { + try { + executor.execute(output); + } catch (StorageException e) { + throw new WebApplicationException(e); + } + }; + return Response.ok(stream) + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=report.xlsx").build(); } } @Path("route") @GET public Collection<Position> getRoute( - @QueryParam("deviceId") final List<Long> deviceIds, @QueryParam("groupId") final List<Long> groupIds, - @QueryParam("from") Date from, @QueryParam("to") Date to) throws SQLException { + @QueryParam("deviceId") List<Long> deviceIds, + @QueryParam("groupId") List<Long> groupIds, + @QueryParam("from") Date from, + @QueryParam("to") Date to) throws StorageException { + permissionsService.checkRestriction(getUserId(), UserRestrictions::getDisableReports); LogAction.logReport(getUserId(), "route", from, to, deviceIds, groupIds); - return Route.getObjects(getUserId(), deviceIds, groupIds, from, to); + return routeReportProvider.getObjects(getUserId(), deviceIds, groupIds, from, to); } @Path("route") @GET - @Produces(XLSX) + @Produces(EXCEL) public Response getRouteExcel( - @QueryParam("deviceId") final List<Long> deviceIds, @QueryParam("groupId") final List<Long> groupIds, - @QueryParam("from") Date from, @QueryParam("to") Date to, @QueryParam("mail") boolean mail) - throws SQLException, IOException { + @QueryParam("deviceId") List<Long> deviceIds, + @QueryParam("groupId") List<Long> groupIds, + @QueryParam("from") Date from, + @QueryParam("to") Date to, + @QueryParam("mail") boolean mail) throws StorageException { + permissionsService.checkRestriction(getUserId(), UserRestrictions::getDisableReports); return executeReport(getUserId(), mail, stream -> { LogAction.logReport(getUserId(), "route", from, to, deviceIds, groupIds); - Route.getExcel(stream, getUserId(), deviceIds, groupIds, from, to); + routeReportProvider.getExcel(stream, getUserId(), deviceIds, groupIds, from, to); }); } + @Path("route/{type:xlsx|mail}") + @GET + @Produces(EXCEL) + public Response getRouteExcel( + @QueryParam("deviceId") List<Long> deviceIds, + @QueryParam("groupId") final List<Long> groupIds, + @QueryParam("from") Date from, + @QueryParam("to") Date to, + @PathParam("type") String type) throws StorageException { + return getRouteExcel(deviceIds, groupIds, from, to, type.equals("mail")); + } + @Path("events") @GET public Collection<Event> getEvents( - @QueryParam("deviceId") final List<Long> deviceIds, @QueryParam("groupId") final List<Long> groupIds, - @QueryParam("type") final List<String> types, - @QueryParam("from") Date from, @QueryParam("to") Date to) throws SQLException { + @QueryParam("deviceId") List<Long> deviceIds, + @QueryParam("groupId") List<Long> groupIds, + @QueryParam("type") List<String> types, + @QueryParam("from") Date from, + @QueryParam("to") Date to) throws StorageException { + permissionsService.checkRestriction(getUserId(), UserRestrictions::getDisableReports); LogAction.logReport(getUserId(), "events", from, to, deviceIds, groupIds); - return Events.getObjects(getUserId(), deviceIds, groupIds, types, from, to); + return eventsReportProvider.getObjects(getUserId(), deviceIds, groupIds, types, from, to); } @Path("events") @GET - @Produces(XLSX) + @Produces(EXCEL) public Response getEventsExcel( - @QueryParam("deviceId") final List<Long> deviceIds, @QueryParam("groupId") final List<Long> groupIds, - @QueryParam("type") final List<String> types, - @QueryParam("from") Date from, @QueryParam("to") Date to, @QueryParam("mail") boolean mail) - throws SQLException, IOException { + @QueryParam("deviceId") List<Long> deviceIds, + @QueryParam("groupId") List<Long> groupIds, + @QueryParam("type") List<String> types, + @QueryParam("from") Date from, + @QueryParam("to") Date to, + @QueryParam("mail") boolean mail) throws StorageException { + permissionsService.checkRestriction(getUserId(), UserRestrictions::getDisableReports); return executeReport(getUserId(), mail, stream -> { LogAction.logReport(getUserId(), "events", from, to, deviceIds, groupIds); - Events.getExcel(stream, getUserId(), deviceIds, groupIds, types, from, to); + eventsReportProvider.getExcel(stream, getUserId(), deviceIds, groupIds, types, from, to); }); } + @Path("events/{type:xlsx|mail}") + @GET + @Produces(EXCEL) + public Response getEventsExcel( + @QueryParam("deviceId") List<Long> deviceIds, + @QueryParam("groupId") List<Long> groupIds, + @QueryParam("type") List<String> types, + @QueryParam("from") Date from, + @QueryParam("to") Date to, + @PathParam("type") String type) throws StorageException { + return getEventsExcel(deviceIds, groupIds, types, from, to, type.equals("mail")); + } + @Path("summary") @GET - public Collection<SummaryReport> getSummary( - @QueryParam("deviceId") final List<Long> deviceIds, @QueryParam("groupId") final List<Long> groupIds, - @QueryParam("from") Date from, @QueryParam("to") Date to, @QueryParam("daily") boolean daily) - throws SQLException { + public Collection<SummaryReportItem> getSummary( + @QueryParam("deviceId") List<Long> deviceIds, + @QueryParam("groupId") List<Long> groupIds, + @QueryParam("from") Date from, + @QueryParam("to") Date to, + @QueryParam("daily") boolean daily) throws StorageException { + permissionsService.checkRestriction(getUserId(), UserRestrictions::getDisableReports); LogAction.logReport(getUserId(), "summary", from, to, deviceIds, groupIds); - return Summary.getObjects(getUserId(), deviceIds, groupIds, from, to, daily); + return summaryReportProvider.getObjects(getUserId(), deviceIds, groupIds, from, to, daily); } @Path("summary") @GET - @Produces(XLSX) + @Produces(EXCEL) public Response getSummaryExcel( - @QueryParam("deviceId") final List<Long> deviceIds, @QueryParam("groupId") final List<Long> groupIds, - @QueryParam("from") Date from, @QueryParam("to") Date to, @QueryParam("daily") boolean daily, - @QueryParam("mail") boolean mail) - throws SQLException, IOException { + @QueryParam("deviceId") List<Long> deviceIds, + @QueryParam("groupId") List<Long> groupIds, + @QueryParam("from") Date from, + @QueryParam("to") Date to, + @QueryParam("daily") boolean daily, + @QueryParam("mail") boolean mail) throws StorageException { + permissionsService.checkRestriction(getUserId(), UserRestrictions::getDisableReports); return executeReport(getUserId(), mail, stream -> { LogAction.logReport(getUserId(), "summary", from, to, deviceIds, groupIds); - Summary.getExcel(stream, getUserId(), deviceIds, groupIds, from, to, daily); + summaryReportProvider.getExcel(stream, getUserId(), deviceIds, groupIds, from, to, daily); }); } + @Path("summary/{type:xlsx|mail}") + @GET + @Produces(EXCEL) + public Response getSummaryExcel( + @QueryParam("deviceId") List<Long> deviceIds, + @QueryParam("groupId") List<Long> groupIds, + @QueryParam("from") Date from, + @QueryParam("to") Date to, + @QueryParam("daily") boolean daily, + @PathParam("type") String type) throws StorageException { + return getSummaryExcel(deviceIds, groupIds, from, to, daily, type.equals("mail")); + } + @Path("trips") @GET - @Produces(MediaType.APPLICATION_JSON) - public Collection<TripReport> getTrips( - @QueryParam("deviceId") final List<Long> deviceIds, @QueryParam("groupId") final List<Long> groupIds, - @QueryParam("from") Date from, @QueryParam("to") Date to) throws SQLException { + public Collection<TripReportItem> getTrips( + @QueryParam("deviceId") List<Long> deviceIds, + @QueryParam("groupId") List<Long> groupIds, + @QueryParam("from") Date from, + @QueryParam("to") Date to) throws StorageException { + permissionsService.checkRestriction(getUserId(), UserRestrictions::getDisableReports); LogAction.logReport(getUserId(), "trips", from, to, deviceIds, groupIds); - return Trips.getObjects(getUserId(), deviceIds, groupIds, from, to); + return tripsReportProvider.getObjects(getUserId(), deviceIds, groupIds, from, to); } @Path("trips") @GET - @Produces(XLSX) + @Produces(EXCEL) public Response getTripsExcel( - @QueryParam("deviceId") final List<Long> deviceIds, @QueryParam("groupId") final List<Long> groupIds, - @QueryParam("from") Date from, @QueryParam("to") Date to, @QueryParam("mail") boolean mail) - throws SQLException, IOException { + @QueryParam("deviceId") List<Long> deviceIds, + @QueryParam("groupId") List<Long> groupIds, + @QueryParam("from") Date from, + @QueryParam("to") Date to, + @QueryParam("mail") boolean mail) throws StorageException { + permissionsService.checkRestriction(getUserId(), UserRestrictions::getDisableReports); return executeReport(getUserId(), mail, stream -> { LogAction.logReport(getUserId(), "trips", from, to, deviceIds, groupIds); - Trips.getExcel(stream, getUserId(), deviceIds, groupIds, from, to); + tripsReportProvider.getExcel(stream, getUserId(), deviceIds, groupIds, from, to); }); } + @Path("trips/{type:xlsx|mail}") + @GET + @Produces(EXCEL) + public Response getTripsExcel( + @QueryParam("deviceId") List<Long> deviceIds, + @QueryParam("groupId") List<Long> groupIds, + @QueryParam("from") Date from, + @QueryParam("to") Date to, + @PathParam("type") String type) throws StorageException { + return getTripsExcel(deviceIds, groupIds, from, to, type.equals("mail")); + } + @Path("stops") @GET - @Produces(MediaType.APPLICATION_JSON) - public Collection<StopReport> getStops( - @QueryParam("deviceId") final List<Long> deviceIds, @QueryParam("groupId") final List<Long> groupIds, - @QueryParam("from") Date from, @QueryParam("to") Date to) throws SQLException { + public Collection<StopReportItem> getStops( + @QueryParam("deviceId") List<Long> deviceIds, + @QueryParam("groupId") List<Long> groupIds, + @QueryParam("from") Date from, + @QueryParam("to") Date to) throws StorageException { + permissionsService.checkRestriction(getUserId(), UserRestrictions::getDisableReports); LogAction.logReport(getUserId(), "stops", from, to, deviceIds, groupIds); - return Stops.getObjects(getUserId(), deviceIds, groupIds, from, to); + return stopsReportProvider.getObjects(getUserId(), deviceIds, groupIds, from, to); } @Path("stops") @GET - @Produces(XLSX) + @Produces(EXCEL) public Response getStopsExcel( - @QueryParam("deviceId") final List<Long> deviceIds, @QueryParam("groupId") final List<Long> groupIds, - @QueryParam("from") Date from, @QueryParam("to") Date to, @QueryParam("mail") boolean mail) - throws SQLException, IOException { + @QueryParam("deviceId") List<Long> deviceIds, + @QueryParam("groupId") List<Long> groupIds, + @QueryParam("from") Date from, + @QueryParam("to") Date to, + @QueryParam("mail") boolean mail) throws StorageException { + permissionsService.checkRestriction(getUserId(), UserRestrictions::getDisableReports); return executeReport(getUserId(), mail, stream -> { LogAction.logReport(getUserId(), "stops", from, to, deviceIds, groupIds); - Stops.getExcel(stream, getUserId(), deviceIds, groupIds, from, to); + stopsReportProvider.getExcel(stream, getUserId(), deviceIds, groupIds, from, to); }); } + @Path("stops/{type:xlsx|mail}") + @GET + @Produces(EXCEL) + public Response getStopsExcel( + @QueryParam("deviceId") List<Long> deviceIds, + @QueryParam("groupId") List<Long> groupIds, + @QueryParam("from") Date from, + @QueryParam("to") Date to, + @PathParam("type") String type) throws StorageException { + return getStopsExcel(deviceIds, groupIds, from, to, type.equals("mail")); + } + } diff --git a/src/main/java/org/traccar/api/resource/ServerResource.java b/src/main/java/org/traccar/api/resource/ServerResource.java index 91488afff..4b7ee9189 100644 --- a/src/main/java/org/traccar/api/resource/ServerResource.java +++ b/src/main/java/org/traccar/api/resource/ServerResource.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 - 2020 Anton Tananaev (anton@traccar.org) + * Copyright 2015 - 2022 Anton Tananaev (anton@traccar.org) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,12 +15,23 @@ */ package org.traccar.api.resource; -import org.traccar.Context; import org.traccar.api.BaseResource; +import org.traccar.helper.model.UserUtil; +import org.traccar.mail.MailManager; +import org.traccar.geocoder.Geocoder; +import org.traccar.helper.Log; import org.traccar.helper.LogAction; import org.traccar.model.Server; +import org.traccar.model.User; +import org.traccar.session.cache.CacheManager; +import org.traccar.storage.StorageException; +import org.traccar.storage.query.Columns; +import org.traccar.storage.query.Condition; +import org.traccar.storage.query.Request; +import javax.annotation.Nullable; import javax.annotation.security.PermitAll; +import javax.inject.Inject; import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.PUT; @@ -29,27 +40,52 @@ import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; -import java.sql.SQLException; +import java.util.Arrays; +import java.util.Collection; +import java.util.TimeZone; @Path("server") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public class ServerResource extends BaseResource { + @Inject + private CacheManager cacheManager; + + @Inject + private MailManager mailManager; + + @Inject + @Nullable + private Geocoder geocoder; + @PermitAll @GET - public Server get(@QueryParam("force") boolean force) throws SQLException { - if (force) { - return Context.getDataManager().getServer(); + public Server get() throws StorageException { + Server server = storage.getObject(Server.class, new Request(new Columns.All())); + server.setEmailEnabled(mailManager.getEmailEnabled()); + server.setGeocoderEnabled(geocoder != null); + User user = permissionsService.getUser(getUserId()); + if (user != null) { + if (user.getAdministrator()) { + server.setStorageSpace(Log.getStorageSpace()); + } } else { - return Context.getPermissionsManager().getServer(); + server.setNewServer(UserUtil.isEmpty(storage)); + } + if (user != null && user.getAdministrator()) { + server.setStorageSpace(Log.getStorageSpace()); } + return server; } @PUT - public Response update(Server entity) throws SQLException { - Context.getPermissionsManager().checkAdmin(getUserId()); - Context.getPermissionsManager().updateServer(entity); + public Response update(Server entity) throws StorageException { + permissionsService.checkAdmin(getUserId()); + storage.updateObject(entity, new Request( + new Columns.Exclude("id"), + new Condition.Equals("id", entity.getId()))); + cacheManager.updateOrInvalidate(true, entity); LogAction.edit(getUserId(), entity); return Response.ok(entity).build(); } @@ -57,11 +93,17 @@ public class ServerResource extends BaseResource { @Path("geocode") @GET public String geocode(@QueryParam("latitude") double latitude, @QueryParam("longitude") double longitude) { - if (Context.getGeocoder() != null) { - return Context.getGeocoder().getAddress(latitude, longitude, null); + if (geocoder != null) { + return geocoder.getAddress(latitude, longitude, null); } else { throw new RuntimeException("Reverse geocoding is not enabled"); } } + @Path("timezones") + @GET + public Collection<String> timezones() { + return Arrays.asList(TimeZone.getAvailableIDs()); + } + } diff --git a/src/main/java/org/traccar/api/resource/SessionResource.java b/src/main/java/org/traccar/api/resource/SessionResource.java index e3c5d457f..7025d5fa7 100644 --- a/src/main/java/org/traccar/api/resource/SessionResource.java +++ b/src/main/java/org/traccar/api/resource/SessionResource.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 Anton Tananaev (anton@traccar.org) + * Copyright 2015 - 2022 Anton Tananaev (anton@traccar.org) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,14 +15,20 @@ */ package org.traccar.api.resource; -import org.traccar.Context; import org.traccar.api.BaseResource; +import org.traccar.api.security.LoginService; +import org.traccar.api.signature.TokenManager; import org.traccar.helper.DataConverter; -import org.traccar.helper.ServletHelper; import org.traccar.helper.LogAction; +import org.traccar.helper.ServletHelper; import org.traccar.model.User; +import org.traccar.storage.StorageException; +import org.traccar.storage.query.Columns; +import org.traccar.storage.query.Condition; +import org.traccar.storage.query.Request; import javax.annotation.security.PermitAll; +import javax.inject.Inject; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.Consumes; @@ -31,16 +37,18 @@ import javax.ws.rs.FormParam; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; +import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; - -import java.io.UnsupportedEncodingException; +import java.io.IOException; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; -import java.sql.SQLException; +import java.security.GeneralSecurityException; +import java.util.Date; @Path("session") @Produces(MediaType.APPLICATION_JSON) @@ -51,60 +59,86 @@ public class SessionResource extends BaseResource { public static final String USER_COOKIE_KEY = "user"; public static final String PASS_COOKIE_KEY = "password"; - @javax.ws.rs.core.Context + @Inject + private LoginService loginService; + + @Inject + private TokenManager tokenManager; + + @Context private HttpServletRequest request; @PermitAll @GET - public User get(@QueryParam("token") String token) throws SQLException, UnsupportedEncodingException { + public User get(@QueryParam("token") String token) throws StorageException, IOException, GeneralSecurityException { + + if (token != null) { + User user = loginService.login(token); + if (user != null) { + request.getSession().setAttribute(USER_ID_KEY, user.getId()); + LogAction.login(user.getId(), ServletHelper.retrieveRemoteAddress(request)); + return user; + } + } + Long userId = (Long) request.getSession().getAttribute(USER_ID_KEY); if (userId == null) { + Cookie[] cookies = request.getCookies(); String email = null, password = null; if (cookies != null) { for (Cookie cookie : cookies) { if (cookie.getName().equals(USER_COOKIE_KEY)) { byte[] emailBytes = DataConverter.parseBase64( - URLDecoder.decode(cookie.getValue(), StandardCharsets.US_ASCII.name())); + URLDecoder.decode(cookie.getValue(), StandardCharsets.US_ASCII)); email = new String(emailBytes, StandardCharsets.UTF_8); } else if (cookie.getName().equals(PASS_COOKIE_KEY)) { byte[] passwordBytes = DataConverter.parseBase64( - URLDecoder.decode(cookie.getValue(), StandardCharsets.US_ASCII.name())); + URLDecoder.decode(cookie.getValue(), StandardCharsets.US_ASCII)); password = new String(passwordBytes, StandardCharsets.UTF_8); } } } if (email != null && password != null) { - User user = Context.getPermissionsManager().login(email, password); - if (user != null) { - userId = user.getId(); - request.getSession().setAttribute(USER_ID_KEY, userId); - } - } else if (token != null) { - User user = Context.getUsersManager().getUserByToken(token); + User user = loginService.login(email, password); if (user != null) { - userId = user.getId(); - request.getSession().setAttribute(USER_ID_KEY, userId); + request.getSession().setAttribute(USER_ID_KEY, user.getId()); + LogAction.login(user.getId(), ServletHelper.retrieveRemoteAddress(request)); + return user; } } - } - if (userId != null) { - Context.getPermissionsManager().checkUserEnabled(userId); - return Context.getPermissionsManager().getUser(userId); } else { - throw new WebApplicationException(Response.status(Response.Status.NOT_FOUND).build()); + + User user = permissionsService.getUser(userId); + if (user != null) { + return user; + } + } + + throw new WebApplicationException(Response.status(Response.Status.NOT_FOUND).build()); + } + + @Path("{id}") + @GET + public User get(@PathParam("id") long userId) throws StorageException { + permissionsService.checkAdmin(getUserId()); + User user = storage.getObject(User.class, new Request( + new Columns.All(), new Condition.Equals("id", userId))); + request.getSession().setAttribute(USER_ID_KEY, user.getId()); + LogAction.login(user.getId(), ServletHelper.retrieveRemoteAddress(request)); + return user; } @PermitAll @POST public User add( - @FormParam("email") String email, @FormParam("password") String password) throws SQLException { - User user = Context.getPermissionsManager().login(email, password); + @FormParam("email") String email, @FormParam("password") String password) throws StorageException { + User user = loginService.login(email, password); if (user != null) { request.getSession().setAttribute(USER_ID_KEY, user.getId()); - LogAction.login(user.getId()); + LogAction.login(user.getId(), ServletHelper.retrieveRemoteAddress(request)); return user; } else { LogAction.failedLogin(ServletHelper.retrieveRemoteAddress(request)); @@ -114,9 +148,16 @@ public class SessionResource extends BaseResource { @DELETE public Response remove() { - LogAction.logout(getUserId()); + LogAction.logout(getUserId(), ServletHelper.retrieveRemoteAddress(request)); request.getSession().removeAttribute(USER_ID_KEY); return Response.noContent().build(); } + @Path("token") + @POST + public String requestToken( + @FormParam("expiration") Date expiration) throws StorageException, GeneralSecurityException, IOException { + return tokenManager.generateToken(getUserId(), expiration); + } + } diff --git a/src/main/java/org/traccar/api/resource/StatisticsResource.java b/src/main/java/org/traccar/api/resource/StatisticsResource.java index 58073e7d1..1f2296f28 100644 --- a/src/main/java/org/traccar/api/resource/StatisticsResource.java +++ b/src/main/java/org/traccar/api/resource/StatisticsResource.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 - 2020 Anton Tananaev (anton@traccar.org) + * Copyright 2016 - 2022 Anton Tananaev (anton@traccar.org) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,9 +15,13 @@ */ package org.traccar.api.resource; -import org.traccar.Context; import org.traccar.api.BaseResource; import org.traccar.model.Statistics; +import org.traccar.storage.StorageException; +import org.traccar.storage.query.Columns; +import org.traccar.storage.query.Condition; +import org.traccar.storage.query.Order; +import org.traccar.storage.query.Request; import javax.ws.rs.Consumes; import javax.ws.rs.GET; @@ -25,7 +29,6 @@ import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; -import java.sql.SQLException; import java.util.Collection; import java.util.Date; @@ -36,9 +39,12 @@ public class StatisticsResource extends BaseResource { @GET public Collection<Statistics> get( - @QueryParam("from") Date from, @QueryParam("to") Date to) throws SQLException { - Context.getPermissionsManager().checkAdmin(getUserId()); - return Context.getDataManager().getStatistics(from, to); + @QueryParam("from") Date from, @QueryParam("to") Date to) throws StorageException { + permissionsService.checkAdmin(getUserId()); + return storage.getObjects(Statistics.class, new Request( + new Columns.All(), + new Condition.Between("captureTime", "from", from, "to", to), + new Order("captureTime"))); } } diff --git a/src/main/java/org/traccar/api/resource/UserResource.java b/src/main/java/org/traccar/api/resource/UserResource.java index d54cc2382..e41ebbe61 100644 --- a/src/main/java/org/traccar/api/resource/UserResource.java +++ b/src/main/java/org/traccar/api/resource/UserResource.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 - 2017 Anton Tananaev (anton@traccar.org) + * Copyright 2015 - 2022 Anton Tananaev (anton@traccar.org) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,15 +15,21 @@ */ package org.traccar.api.resource; -import org.traccar.Context; import org.traccar.api.BaseObjectResource; +import org.traccar.config.Config; import org.traccar.config.Keys; -import org.traccar.database.UsersManager; import org.traccar.helper.LogAction; +import org.traccar.helper.model.UserUtil; import org.traccar.model.ManagedUser; +import org.traccar.model.Permission; import org.traccar.model.User; +import org.traccar.storage.StorageException; +import org.traccar.storage.query.Columns; +import org.traccar.storage.query.Condition; +import org.traccar.storage.query.Request; import javax.annotation.security.PermitAll; +import javax.inject.Inject; import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.POST; @@ -32,63 +38,82 @@ import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; -import java.sql.SQLException; import java.util.Collection; import java.util.Date; -import java.util.Set; @Path("users") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public class UserResource extends BaseObjectResource<User> { + @Inject + private Config config; + public UserResource() { super(User.class); } @GET - public Collection<User> get(@QueryParam("userId") long userId) throws SQLException { - UsersManager usersManager = Context.getUsersManager(); - Set<Long> result; - if (Context.getPermissionsManager().getUserAdmin(getUserId())) { - if (userId != 0) { - result = usersManager.getUserItems(userId); - } else { - result = usersManager.getAllItems(); - } - } else if (Context.getPermissionsManager().getUserManager(getUserId())) { - result = usersManager.getManagedItems(getUserId()); + public Collection<User> get(@QueryParam("userId") long userId) throws StorageException { + if (userId > 0) { + permissionsService.checkUser(getUserId(), userId); + return storage.getObjects(baseClass, new Request( + new Columns.All(), + new Condition.Permission(User.class, userId, ManagedUser.class).excludeGroups())); + } else if (permissionsService.notAdmin(getUserId())) { + return storage.getObjects(baseClass, new Request( + new Columns.All(), + new Condition.Permission(User.class, getUserId(), ManagedUser.class).excludeGroups())); } else { - throw new SecurityException("Admin or manager access required"); + return storage.getObjects(baseClass, new Request(new Columns.All())); } - return usersManager.getItems(result); } @Override @PermitAll @POST - public Response add(User entity) throws SQLException { - if (!Context.getPermissionsManager().getUserAdmin(getUserId())) { - Context.getPermissionsManager().checkUserUpdate(getUserId(), new User(), entity); - if (Context.getPermissionsManager().getUserManager(getUserId())) { - Context.getPermissionsManager().checkUserLimit(getUserId()); + public Response add(User entity) throws StorageException { + User currentUser = getUserId() > 0 ? permissionsService.getUser(getUserId()) : null; + if (currentUser == null || !currentUser.getAdministrator()) { + permissionsService.checkUserUpdate(getUserId(), new User(), entity); + if (currentUser != null && currentUser.getUserLimit() != 0) { + int userLimit = currentUser.getUserLimit(); + if (userLimit > 0) { + int userCount = storage.getObjects(baseClass, new Request( + new Columns.All(), + new Condition.Permission(User.class, getUserId(), ManagedUser.class).excludeGroups())) + .size(); + if (userCount >= userLimit) { + throw new SecurityException("Manager user limit reached"); + } + } } else { - Context.getPermissionsManager().checkRegistration(getUserId()); - entity.setDeviceLimit(Context.getConfig().getInteger(Keys.USERS_DEFAULT_DEVICE_LIMIT)); - int expirationDays = Context.getConfig().getInteger(Keys.USERS_DEFAULT_EXPIRATION_DAYS); + if (!permissionsService.getServer().getRegistration()) { + throw new SecurityException("Registration disabled"); + } + entity.setDeviceLimit(config.getInteger(Keys.USERS_DEFAULT_DEVICE_LIMIT)); + int expirationDays = config.getInteger(Keys.USERS_DEFAULT_EXPIRATION_DAYS); if (expirationDays > 0) { - entity.setExpirationTime( - new Date(System.currentTimeMillis() + (long) expirationDays * 24 * 3600 * 1000)); + entity.setExpirationTime(new Date(System.currentTimeMillis() + expirationDays * 86400000L)); } } } - Context.getUsersManager().addItem(entity); + + if (UserUtil.isEmpty(storage)) { + entity.setAdministrator(true); + } + + entity.setId(storage.addObject(entity, new Request(new Columns.Exclude("id")))); + storage.updateObject(entity, new Request( + new Columns.Include("hashedPassword", "salt"), + new Condition.Equals("id", entity.getId()))); + LogAction.create(getUserId(), entity); - if (Context.getPermissionsManager().getUserManager(getUserId())) { - Context.getDataManager().linkObject(User.class, getUserId(), ManagedUser.class, entity.getId(), true); + + if (currentUser != null && currentUser.getUserLimit() != 0) { + storage.addPermission(new Permission(User.class, getUserId(), ManagedUser.class, entity.getId())); LogAction.link(getUserId(), User.class, getUserId(), ManagedUser.class, entity.getId()); } - Context.getUsersManager().refreshUserItems(); return Response.ok(entity).build(); } diff --git a/src/main/java/org/traccar/api/security/LoginService.java b/src/main/java/org/traccar/api/security/LoginService.java new file mode 100644 index 000000000..88bafcfb5 --- /dev/null +++ b/src/main/java/org/traccar/api/security/LoginService.java @@ -0,0 +1,99 @@ +/* + * Copyright 2022 Anton Tananaev (anton@traccar.org) + * + * 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.traccar.api.security; + +import org.traccar.api.signature.TokenManager; +import org.traccar.config.Config; +import org.traccar.config.Keys; +import org.traccar.database.LdapProvider; +import org.traccar.model.User; +import org.traccar.storage.Storage; +import org.traccar.storage.StorageException; +import org.traccar.storage.query.Columns; +import org.traccar.storage.query.Condition; +import org.traccar.storage.query.Request; + +import javax.annotation.Nullable; +import javax.inject.Inject; +import javax.inject.Singleton; +import java.io.IOException; +import java.security.GeneralSecurityException; + +@Singleton +public class LoginService { + + private final Storage storage; + private final TokenManager tokenManager; + private final LdapProvider ldapProvider; + + private final String serviceAccountToken; + private final boolean forceLdap; + + @Inject + public LoginService( + Config config, Storage storage, TokenManager tokenManager, @Nullable LdapProvider ldapProvider) { + this.storage = storage; + this.tokenManager = tokenManager; + this.ldapProvider = ldapProvider; + serviceAccountToken = config.getString(Keys.WEB_SERVICE_ACCOUNT_TOKEN); + forceLdap = config.getBoolean(Keys.LDAP_FORCE); + } + + public User login(String token) throws StorageException, GeneralSecurityException, IOException { + if (serviceAccountToken != null && serviceAccountToken.equals(token)) { + return new ServiceAccountUser(); + } + long userId = tokenManager.verifyToken(token); + User user = storage.getObject(User.class, new Request( + new Columns.All(), new Condition.Equals("id", userId))); + if (user != null) { + checkUserEnabled(user); + } + return user; + } + + public User login(String email, String password) throws StorageException { + email = email.trim(); + User user = storage.getObject(User.class, new Request( + new Columns.All(), + new Condition.Or( + new Condition.Equals("email", email), + new Condition.Equals("login", email)))); + if (user != null) { + if (ldapProvider != null && user.getLogin() != null && ldapProvider.login(user.getLogin(), password) + || !forceLdap && user.isPasswordValid(password)) { + checkUserEnabled(user); + return user; + } + } else { + if (ldapProvider != null && ldapProvider.login(email, password)) { + user = ldapProvider.getUser(email); + user.setId(storage.addObject(user, new Request(new Columns.Exclude("id")))); + checkUserEnabled(user); + return user; + } + } + return null; + } + + private void checkUserEnabled(User user) throws SecurityException { + if (user == null) { + throw new SecurityException("Unknown account"); + } + user.checkDisabled(); + } + +} diff --git a/src/main/java/org/traccar/api/security/PermissionsService.java b/src/main/java/org/traccar/api/security/PermissionsService.java new file mode 100644 index 000000000..4421572d7 --- /dev/null +++ b/src/main/java/org/traccar/api/security/PermissionsService.java @@ -0,0 +1,211 @@ +/* + * Copyright 2022 Anton Tananaev (anton@traccar.org) + * + * 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.traccar.api.security; + +import com.google.inject.servlet.RequestScoped; +import org.traccar.model.BaseModel; +import org.traccar.model.Calendar; +import org.traccar.model.Command; +import org.traccar.model.Device; +import org.traccar.model.Group; +import org.traccar.model.GroupedModel; +import org.traccar.model.ManagedUser; +import org.traccar.model.ScheduledModel; +import org.traccar.model.Server; +import org.traccar.model.User; +import org.traccar.model.UserRestrictions; +import org.traccar.storage.Storage; +import org.traccar.storage.StorageException; +import org.traccar.storage.query.Columns; +import org.traccar.storage.query.Condition; +import org.traccar.storage.query.Request; + +import javax.inject.Inject; +import java.util.Objects; + +@RequestScoped +public class PermissionsService { + + private final Storage storage; + + private Server server; + private User user; + + @Inject + public PermissionsService(Storage storage) { + this.storage = storage; + } + + public Server getServer() throws StorageException { + if (server == null) { + server = storage.getObject( + Server.class, new Request(new Columns.All())); + } + return server; + } + + public User getUser(long userId) throws StorageException { + if (user == null && userId > 0) { + if (userId == ServiceAccountUser.ID) { + user = new ServiceAccountUser(); + } else { + user = storage.getObject( + User.class, new Request(new Columns.All(), new Condition.Equals("id", userId))); + } + } + return user; + } + + public boolean notAdmin(long userId) throws StorageException { + return !getUser(userId).getAdministrator(); + } + + public void checkAdmin(long userId) throws StorageException, SecurityException { + if (!getUser(userId).getAdministrator()) { + throw new SecurityException("Administrator access required"); + } + } + + public void checkManager(long userId) throws StorageException, SecurityException { + if (!getUser(userId).getAdministrator() && getUser(userId).getUserLimit() == 0) { + throw new SecurityException("Manager access required"); + } + } + + public interface CheckRestrictionCallback { + boolean denied(UserRestrictions userRestrictions); + } + + public void checkRestriction( + long userId, CheckRestrictionCallback callback) throws StorageException, SecurityException { + if (!getUser(userId).getAdministrator() + && (callback.denied(getServer()) || callback.denied(getUser(userId)))) { + throw new SecurityException("Operation restricted"); + } + } + + public void checkEdit(long userId, Class<?> clazz, boolean addition) throws StorageException, SecurityException { + if (!getUser(userId).getAdministrator()) { + boolean denied = false; + if (getServer().getReadonly() || getUser(userId).getReadonly()) { + denied = true; + } else if (clazz.equals(Device.class)) { + denied = getServer().getDeviceReadonly() || getUser(userId).getDeviceReadonly() + || addition && getUser(userId).getDeviceLimit() == 0; + if (!denied && addition && getUser(userId).getDeviceLimit() > 0) { + int deviceCount = storage.getObjects(Device.class, new Request( + new Columns.Include("id"), + new Condition.Permission(User.class, userId, Device.class))).size(); + denied = deviceCount >= getUser(userId).getDeviceLimit(); + } + } else if (clazz.equals(Command.class)) { + denied = getServer().getLimitCommands() || getUser(userId).getLimitCommands(); + } + if (denied) { + throw new SecurityException("Write access denied"); + } + } + } + + public void checkEdit(long userId, BaseModel object, boolean addition) throws StorageException, SecurityException { + if (!getUser(userId).getAdministrator()) { + checkEdit(userId, object.getClass(), addition); + if (object instanceof GroupedModel) { + GroupedModel after = ((GroupedModel) object); + if (after.getGroupId() > 0) { + GroupedModel before = null; + if (!addition) { + before = storage.getObject(after.getClass(), new Request( + new Columns.Include("groupId"), new Condition.Equals("id", object.getId()))); + } + if (before == null || before.getGroupId() != after.getGroupId()) { + checkPermission(Group.class, userId, after.getGroupId()); + } + } + } + if (object instanceof ScheduledModel) { + ScheduledModel after = ((ScheduledModel) object); + if (after.getCalendarId() > 0) { + ScheduledModel before = null; + if (!addition) { + before = storage.getObject(after.getClass(), new Request( + new Columns.Include("calendarId"), new Condition.Equals("id", object.getId()))); + } + if (before == null || before.getCalendarId() != after.getCalendarId()) { + checkPermission(Calendar.class, userId, after.getCalendarId()); + } + } + } + } + } + + public void checkUser(long userId, long managedUserId) throws StorageException, SecurityException { + if (userId != managedUserId && !getUser(userId).getAdministrator()) { + if (!getUser(userId).getManager() + || storage.getPermissions(User.class, userId, ManagedUser.class, managedUserId).isEmpty()) { + throw new SecurityException("User access denied"); + } + } + } + + public void checkUserUpdate(long userId, User before, User after) throws StorageException, SecurityException { + if (before.getAdministrator() != after.getAdministrator() + || before.getDeviceLimit() != after.getDeviceLimit() + || before.getUserLimit() != after.getUserLimit()) { + checkAdmin(userId); + } + User user = getUser(userId); + if (user != null && user.getExpirationTime() != null + && !Objects.equals(before.getExpirationTime(), after.getExpirationTime()) + && (after.getExpirationTime() == null + || user.getExpirationTime().compareTo(after.getExpirationTime()) < 0)) { + checkAdmin(userId); + } + if (before.getReadonly() != after.getReadonly() + || before.getDeviceReadonly() != after.getDeviceReadonly() + || before.getDisabled() != after.getDisabled() + || before.getLimitCommands() != after.getLimitCommands() + || before.getDisableReports() != after.getDisableReports() + || before.getFixedEmail() != after.getFixedEmail()) { + if (userId == after.getId()) { + checkAdmin(userId); + } else if (after.getId() > 0) { + checkUser(userId, after.getId()); + } else { + checkManager(userId); + } + } + if (before.getFixedEmail() && !before.getEmail().equals(after.getEmail())) { + checkAdmin(userId); + } + } + + public <T extends BaseModel> void checkPermission( + Class<T> clazz, long userId, long objectId) throws StorageException, SecurityException { + if (!getUser(userId).getAdministrator() && !(clazz.equals(User.class) && userId == objectId)) { + var object = storage.getObject(clazz, new Request( + new Columns.Include("id"), + new Condition.And( + new Condition.Equals("id", objectId), + new Condition.Permission( + User.class, userId, clazz.equals(User.class) ? ManagedUser.class : clazz)))); + if (object == null) { + throw new SecurityException(clazz.getSimpleName() + " access denied"); + } + } + } + +} diff --git a/src/main/java/org/traccar/api/SecurityRequestFilter.java b/src/main/java/org/traccar/api/security/SecurityRequestFilter.java index 33b6b37df..94b6bbf05 100644 --- a/src/main/java/org/traccar/api/SecurityRequestFilter.java +++ b/src/main/java/org/traccar/api/security/SecurityRequestFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 - 2016 Anton Tananaev (anton@traccar.org) + * Copyright 2015 - 2022 Anton Tananaev (anton@traccar.org) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,28 +13,30 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.traccar.api; +package org.traccar.api.security; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.traccar.Context; -import org.traccar.Main; import org.traccar.api.resource.SessionResource; import org.traccar.database.StatisticsManager; import org.traccar.helper.DataConverter; import org.traccar.model.User; +import org.traccar.storage.StorageException; import javax.annotation.security.PermitAll; +import javax.inject.Inject; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.WebApplicationException; import javax.ws.rs.container.ContainerRequestContext; import javax.ws.rs.container.ContainerRequestFilter; import javax.ws.rs.container.ResourceInfo; +import javax.ws.rs.core.Context; import javax.ws.rs.core.Response; import javax.ws.rs.core.SecurityContext; +import java.io.IOException; import java.lang.reflect.Method; import java.nio.charset.StandardCharsets; -import java.sql.SQLException; +import java.security.GeneralSecurityException; public class SecurityRequestFilter implements ContainerRequestFilter { @@ -43,6 +45,7 @@ public class SecurityRequestFilter implements ContainerRequestFilter { public static final String AUTHORIZATION_HEADER = "Authorization"; public static final String WWW_AUTHENTICATE = "WWW-Authenticate"; public static final String BASIC_REALM = "Basic realm=\"api\""; + public static final String BEARER_PREFIX = "Bearer "; public static final String X_REQUESTED_WITH = "X-Requested-With"; public static final String XML_HTTP_REQUEST = "XMLHttpRequest"; @@ -55,12 +58,18 @@ public class SecurityRequestFilter implements ContainerRequestFilter { return null; } - @javax.ws.rs.core.Context + @Context private HttpServletRequest request; - @javax.ws.rs.core.Context + @Context private ResourceInfo resourceInfo; + @Inject + private LoginService loginService; + + @Inject + private StatisticsManager statisticsManager; + @Override public void filter(ContainerRequestContext requestContext) { @@ -76,13 +85,18 @@ public class SecurityRequestFilter implements ContainerRequestFilter { if (authHeader != null) { try { - String[] auth = decodeBasicAuth(authHeader); - User user = Context.getPermissionsManager().login(auth[0], auth[1]); + User user; + if (authHeader.startsWith(BEARER_PREFIX)) { + user = loginService.login(authHeader.substring(BEARER_PREFIX.length())); + } else { + String[] auth = decodeBasicAuth(authHeader); + user = loginService.login(auth[0], auth[1]); + } if (user != null) { - Main.getInjector().getInstance(StatisticsManager.class).registerRequest(user.getId()); + statisticsManager.registerRequest(user.getId()); securityContext = new UserSecurityContext(new UserPrincipal(user.getId())); } - } catch (SQLException e) { + } catch (StorageException | GeneralSecurityException | IOException e) { throw new WebApplicationException(e); } @@ -90,8 +104,7 @@ public class SecurityRequestFilter implements ContainerRequestFilter { Long userId = (Long) request.getSession().getAttribute(SessionResource.USER_ID_KEY); if (userId != null) { - Context.getPermissionsManager().checkUserEnabled(userId); - Main.getInjector().getInstance(StatisticsManager.class).registerRequest(userId); + statisticsManager.registerRequest(userId); securityContext = new UserSecurityContext(new UserPrincipal(userId)); } diff --git a/src/main/java/org/traccar/api/ObjectMapperProvider.java b/src/main/java/org/traccar/api/security/ServiceAccountUser.java index f81b20917..644142434 100644 --- a/src/main/java/org/traccar/api/ObjectMapperProvider.java +++ b/src/main/java/org/traccar/api/security/ServiceAccountUser.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 - 2016 Anton Tananaev (anton@traccar.org) + * Copyright 2022 Anton Tananaev (anton@traccar.org) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,20 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.traccar.api; +package org.traccar.api.security; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.traccar.Context; +import org.traccar.model.User; -import javax.ws.rs.ext.ContextResolver; -import javax.ws.rs.ext.Provider; +public class ServiceAccountUser extends User { -@Provider -public class ObjectMapperProvider implements ContextResolver<ObjectMapper> { + public static final long ID = 9000000000000000000L; - @Override - public ObjectMapper getContext(Class<?> type) { - return Context.getObjectMapper(); + public ServiceAccountUser() { + setId(ID); + setName("Service Account"); + setEmail("none"); + setAdministrator(true); } - } diff --git a/src/main/java/org/traccar/api/UserPrincipal.java b/src/main/java/org/traccar/api/security/UserPrincipal.java index 175bca391..18b84a0e1 100644 --- a/src/main/java/org/traccar/api/UserPrincipal.java +++ b/src/main/java/org/traccar/api/security/UserPrincipal.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.traccar.api; +package org.traccar.api.security; import java.security.Principal; diff --git a/src/main/java/org/traccar/api/UserSecurityContext.java b/src/main/java/org/traccar/api/security/UserSecurityContext.java index 55c0621bc..97df6b6c7 100644 --- a/src/main/java/org/traccar/api/UserSecurityContext.java +++ b/src/main/java/org/traccar/api/security/UserSecurityContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 Anton Tananaev (anton@traccar.org) + * Copyright 2015 - 2022 Anton Tananaev (anton@traccar.org) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.traccar.api; +package org.traccar.api.security; import javax.ws.rs.core.SecurityContext; import java.security.Principal; public class UserSecurityContext implements SecurityContext { - private UserPrincipal principal; + private final UserPrincipal principal; public UserSecurityContext(UserPrincipal principal) { this.principal = principal; diff --git a/src/main/java/org/traccar/api/signature/CryptoManager.java b/src/main/java/org/traccar/api/signature/CryptoManager.java new file mode 100644 index 000000000..249d5bd97 --- /dev/null +++ b/src/main/java/org/traccar/api/signature/CryptoManager.java @@ -0,0 +1,103 @@ +/* + * Copyright 2022 Anton Tananaev (anton@traccar.org) + * + * 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.traccar.api.signature; + +import org.traccar.storage.Storage; +import org.traccar.storage.StorageException; +import org.traccar.storage.query.Columns; +import org.traccar.storage.query.Request; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.Signature; +import java.security.spec.ECGenParameterSpec; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; + +@Singleton +public class CryptoManager { + + private final Storage storage; + + private PublicKey publicKey; + private PrivateKey privateKey; + + @Inject + public CryptoManager(Storage storage) { + this.storage = storage; + } + + public byte[] sign(byte[] data) throws GeneralSecurityException, StorageException { + if (privateKey == null) { + initializeKeys(); + } + Signature signature = Signature.getInstance("SHA256withECDSA"); + signature.initSign(privateKey); + signature.update(data); + byte[] block = signature.sign(); + byte[] combined = new byte[1 + block.length + data.length]; + combined[0] = (byte) block.length; + System.arraycopy(block, 0, combined, 1, block.length); + System.arraycopy(data, 0, combined, 1 + block.length, data.length); + return combined; + } + + public byte[] verify(byte[] data) throws GeneralSecurityException, StorageException { + if (publicKey == null) { + initializeKeys(); + } + Signature signature = Signature.getInstance("SHA256withECDSA"); + signature.initVerify(publicKey); + int length = data[0]; + byte[] originalData = new byte[data.length - 1 - length]; + System.arraycopy(data, 1 + length, originalData, 0, originalData.length); + signature.update(originalData); + if (!signature.verify(data, 1, length)) { + throw new SecurityException("Invalid signature"); + } + return originalData; + } + + private void initializeKeys() throws StorageException, GeneralSecurityException { + KeystoreModel model = storage.getObject(KeystoreModel.class, new Request(new Columns.All())); + if (model != null) { + publicKey = KeyFactory.getInstance("EC") + .generatePublic(new X509EncodedKeySpec(model.getPublicKey())); + privateKey = KeyFactory.getInstance("EC") + .generatePrivate(new PKCS8EncodedKeySpec(model.getPrivateKey())); + } else { + KeyPairGenerator generator = KeyPairGenerator.getInstance("EC"); + generator.initialize(new ECGenParameterSpec("secp256r1"), new SecureRandom()); + KeyPair pair = generator.generateKeyPair(); + + publicKey = pair.getPublic(); + privateKey = pair.getPrivate(); + + model = new KeystoreModel(); + model.setPublicKey(publicKey.getEncoded()); + model.setPrivateKey(privateKey.getEncoded()); + storage.addObject(model, new Request(new Columns.Exclude("id"))); + } + } + +} diff --git a/src/main/java/org/traccar/api/signature/KeystoreModel.java b/src/main/java/org/traccar/api/signature/KeystoreModel.java new file mode 100644 index 000000000..7f3140e81 --- /dev/null +++ b/src/main/java/org/traccar/api/signature/KeystoreModel.java @@ -0,0 +1,44 @@ +/* + * Copyright 2022 Anton Tananaev (anton@traccar.org) + * + * 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.traccar.api.signature; + +import org.traccar.model.BaseModel; +import org.traccar.storage.StorageName; + +@StorageName("tc_keystore") +public class KeystoreModel extends BaseModel { + + private byte[] publicKey; + + public byte[] getPublicKey() { + return publicKey; + } + + public void setPublicKey(byte[] publicKey) { + this.publicKey = publicKey; + } + + private byte[] privateKey; + + public byte[] getPrivateKey() { + return privateKey; + } + + public void setPrivateKey(byte[] privateKey) { + this.privateKey = privateKey; + } + +} diff --git a/src/main/java/org/traccar/api/signature/TokenManager.java b/src/main/java/org/traccar/api/signature/TokenManager.java new file mode 100644 index 000000000..6a0d90b40 --- /dev/null +++ b/src/main/java/org/traccar/api/signature/TokenManager.java @@ -0,0 +1,77 @@ +/* + * Copyright 2022 Anton Tananaev (anton@traccar.org) + * + * 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.traccar.api.signature; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.codec.binary.Base64; +import org.traccar.storage.StorageException; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.util.Date; +import java.util.concurrent.TimeUnit; + +@Singleton +public class TokenManager { + + private static final int DEFAULT_EXPIRATION_DAYS = 7; + + private final ObjectMapper objectMapper; + private final CryptoManager cryptoManager; + + public static class Data { + @JsonProperty("u") + private long userId; + @JsonProperty("e") + private Date expiration; + } + + @Inject + public TokenManager(ObjectMapper objectMapper, CryptoManager cryptoManager) { + this.objectMapper = objectMapper; + this.cryptoManager = cryptoManager; + } + + public String generateToken(long userId) throws IOException, GeneralSecurityException, StorageException { + return generateToken(userId, null); + } + + public String generateToken( + long userId, Date expiration) throws IOException, GeneralSecurityException, StorageException { + Data data = new Data(); + data.userId = userId; + if (expiration != null) { + data.expiration = expiration; + } else { + data.expiration = new Date(System.currentTimeMillis() + TimeUnit.DAYS.toMillis(DEFAULT_EXPIRATION_DAYS)); + } + byte[] encoded = objectMapper.writeValueAsBytes(data); + return Base64.encodeBase64URLSafeString(cryptoManager.sign(encoded)); + } + + public long verifyToken(String token) throws IOException, GeneralSecurityException, StorageException { + byte[] encoded = cryptoManager.verify(Base64.decodeBase64(token)); + Data data = objectMapper.readValue(encoded, Data.class); + if (data.expiration.before(new Date())) { + throw new SecurityException("Token has expired"); + } + return data.userId; + } + +} |