diff options
Diffstat (limited to 'src/main/java/org/traccar/api')
38 files changed, 779 insertions, 384 deletions
diff --git a/src/main/java/org/traccar/api/AsyncSocket.java b/src/main/java/org/traccar/api/AsyncSocket.java index 5fc4b4412..f5fbcbf62 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 - 2022 Anton Tananaev (anton@traccar.org) + * Copyright 2015 - 2023 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. @@ -22,16 +22,17 @@ import org.eclipse.jetty.websocket.api.WebSocketAdapter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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.LogRecord; import org.traccar.model.Position; +import org.traccar.session.ConnectionManager; import org.traccar.storage.Storage; import org.traccar.storage.StorageException; import java.util.Collection; -import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; public class AsyncSocket extends WebSocketAdapter implements ConnectionManager.UpdateListener { @@ -41,12 +42,15 @@ public class AsyncSocket extends WebSocketAdapter implements ConnectionManager.U private static final String KEY_DEVICES = "devices"; private static final String KEY_POSITIONS = "positions"; private static final String KEY_EVENTS = "events"; + private static final String KEY_LOGS = "logs"; private final ObjectMapper objectMapper; private final ConnectionManager connectionManager; private final Storage storage; private final long userId; + private boolean includeLogs; + public AsyncSocket(ObjectMapper objectMapper, ConnectionManager connectionManager, Storage storage, long userId) { this.objectMapper = objectMapper; this.connectionManager = connectionManager; @@ -76,29 +80,41 @@ public class AsyncSocket extends WebSocketAdapter implements ConnectionManager.U } @Override + public void onWebSocketText(String message) { + super.onWebSocketText(message); + + try { + includeLogs = objectMapper.readTree(message).get("logs").asBoolean(); + } catch (JsonProcessingException e) { + LOGGER.warn("Socket JSON parsing error", e); + } + } + + @Override public void onKeepalive() { sendData(new HashMap<>()); } @Override public void onUpdateDevice(Device device) { - Map<String, Collection<?>> data = new HashMap<>(); - data.put(KEY_DEVICES, Collections.singletonList(device)); - sendData(data); + sendData(Map.of(KEY_DEVICES, List.of(device))); } @Override public void onUpdatePosition(Position position) { - Map<String, Collection<?>> data = new HashMap<>(); - data.put(KEY_POSITIONS, Collections.singletonList(position)); - sendData(data); + sendData(Map.of(KEY_POSITIONS, List.of(position))); } @Override public void onUpdateEvent(Event event) { - Map<String, Collection<?>> data = new HashMap<>(); - data.put(KEY_EVENTS, Collections.singletonList(event)); - sendData(data); + sendData(Map.of(KEY_EVENTS, List.of(event))); + } + + @Override + public void onUpdateLog(LogRecord record) { + if (includeLogs) { + sendData(Map.of(KEY_LOGS, List.of(record))); + } } private void sendData(Map<String, Collection<?>> data) { diff --git a/src/main/java/org/traccar/api/AsyncSocketServlet.java b/src/main/java/org/traccar/api/AsyncSocketServlet.java index 91a745eeb..cd2c1639e 100644 --- a/src/main/java/org/traccar/api/AsyncSocketServlet.java +++ b/src/main/java/org/traccar/api/AsyncSocketServlet.java @@ -24,9 +24,9 @@ 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 jakarta.inject.Inject; +import jakarta.inject.Singleton; +import jakarta.servlet.http.HttpSession; import java.time.Duration; @Singleton diff --git a/src/main/java/org/traccar/api/BaseObjectResource.java b/src/main/java/org/traccar/api/BaseObjectResource.java index 904781e54..2a801221b 100644 --- a/src/main/java/org/traccar/api/BaseObjectResource.java +++ b/src/main/java/org/traccar/api/BaseObjectResource.java @@ -16,6 +16,8 @@ */ package org.traccar.api; +import org.traccar.api.security.ServiceAccountUser; +import org.traccar.model.ObjectOperation; import org.traccar.helper.LogAction; import org.traccar.model.BaseModel; import org.traccar.model.Group; @@ -28,14 +30,14 @@ 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; -import javax.ws.rs.PUT; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.core.Response; +import jakarta.inject.Inject; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.core.Response; public abstract class BaseObjectResource<T extends BaseModel> extends BaseResource { @@ -65,22 +67,25 @@ public abstract class BaseObjectResource<T extends BaseModel> extends BaseResour } @POST - public Response add(T entity) throws StorageException { + public Response add(T entity) throws Exception { permissionsService.checkEdit(getUserId(), entity, true); entity.setId(storage.addObject(entity, new Request(new Columns.Exclude("id")))); LogAction.create(getUserId(), entity); - 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 (getUserId() != ServiceAccountUser.ID) { + storage.addPermission(new Permission(User.class, getUserId(), baseClass, entity.getId())); + cacheManager.invalidatePermission(true, User.class, getUserId(), baseClass, entity.getId(), true); + connectionManager.invalidatePermission(true, User.class, getUserId(), baseClass, entity.getId(), true); + LogAction.link(getUserId(), User.class, getUserId(), baseClass, entity.getId()); + } return Response.ok(entity).build(); } @Path("{id}") @PUT - public Response update(T entity) throws StorageException { + public Response update(T entity) throws Exception { permissionsService.checkEdit(getUserId(), entity, false); permissionsService.checkPermission(baseClass, getUserId(), entity.getId()); @@ -106,7 +111,7 @@ public abstract class BaseObjectResource<T extends BaseModel> extends BaseResour new Condition.Equals("id", entity.getId()))); } } - cacheManager.updateOrInvalidate(true, entity); + cacheManager.invalidateObject(true, entity.getClass(), entity.getId(), ObjectOperation.UPDATE); LogAction.edit(getUserId(), entity); return Response.ok(entity).build(); @@ -114,12 +119,12 @@ public abstract class BaseObjectResource<T extends BaseModel> extends BaseResour @Path("{id}") @DELETE - public Response remove(@PathParam("id") long id) throws StorageException { + public Response remove(@PathParam("id") long id) throws Exception { permissionsService.checkEdit(getUserId(), baseClass, false); permissionsService.checkPermission(baseClass, getUserId(), id); storage.removeObject(baseClass, new Request(new Condition.Equals("id", id))); - cacheManager.invalidate(baseClass, id); + cacheManager.invalidateObject(true, baseClass, id, ObjectOperation.DELETE); LogAction.remove(getUserId(), baseClass, id); diff --git a/src/main/java/org/traccar/api/BaseResource.java b/src/main/java/org/traccar/api/BaseResource.java index 33abe73fa..f20b8c4c2 100644 --- a/src/main/java/org/traccar/api/BaseResource.java +++ b/src/main/java/org/traccar/api/BaseResource.java @@ -19,9 +19,9 @@ 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; +import jakarta.inject.Inject; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.SecurityContext; public class BaseResource { diff --git a/src/main/java/org/traccar/api/CorsResponseFilter.java b/src/main/java/org/traccar/api/CorsResponseFilter.java index 67d0341a1..a380eb41d 100644 --- a/src/main/java/org/traccar/api/CorsResponseFilter.java +++ b/src/main/java/org/traccar/api/CorsResponseFilter.java @@ -19,11 +19,11 @@ import io.netty.handler.codec.http.HttpHeaderNames; 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 jakarta.inject.Inject; +import jakarta.inject.Singleton; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.ContainerResponseContext; +import jakarta.ws.rs.container.ContainerResponseFilter; import java.io.IOException; @Singleton diff --git a/src/main/java/org/traccar/api/DateParameterConverterProvider.java b/src/main/java/org/traccar/api/DateParameterConverterProvider.java index f07ece984..4858fcbfd 100644 --- a/src/main/java/org/traccar/api/DateParameterConverterProvider.java +++ b/src/main/java/org/traccar/api/DateParameterConverterProvider.java @@ -17,8 +17,8 @@ package org.traccar.api; import org.traccar.helper.DateUtil; -import javax.ws.rs.ext.ParamConverter; -import javax.ws.rs.ext.ParamConverterProvider; +import jakarta.ws.rs.ext.ParamConverter; +import jakarta.ws.rs.ext.ParamConverterProvider; import java.lang.annotation.Annotation; import java.lang.reflect.Type; import java.util.Date; diff --git a/src/main/java/org/traccar/api/ExtendedObjectResource.java b/src/main/java/org/traccar/api/ExtendedObjectResource.java index 8467b46c6..348ebe38a 100644 --- a/src/main/java/org/traccar/api/ExtendedObjectResource.java +++ b/src/main/java/org/traccar/api/ExtendedObjectResource.java @@ -25,8 +25,8 @@ 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 jakarta.ws.rs.GET; +import jakarta.ws.rs.QueryParam; import java.util.Collection; import java.util.LinkedList; diff --git a/src/main/java/org/traccar/api/MediaFilter.java b/src/main/java/org/traccar/api/MediaFilter.java index ab75bdc5d..38d13078d 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 - 2022 Anton Tananaev (anton@traccar.org) + * Copyright 2018 - 2023 Anton Tananaev (anton@traccar.org) * Copyright 2018 Andrey Kunitsyn (andrey@traccar.org) * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -28,17 +28,16 @@ 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; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; import java.io.IOException; @Singleton @@ -58,10 +57,6 @@ public class MediaFilter implements Filter { } @Override - public void init(FilterConfig filterConfig) throws ServletException { - } - - @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { @@ -99,8 +94,4 @@ public class MediaFilter implements Filter { } } - @Override - public void destroy() { - } - } diff --git a/src/main/java/org/traccar/api/ResourceErrorHandler.java b/src/main/java/org/traccar/api/ResourceErrorHandler.java index 108a8e8cc..387f3db6a 100644 --- a/src/main/java/org/traccar/api/ResourceErrorHandler.java +++ b/src/main/java/org/traccar/api/ResourceErrorHandler.java @@ -17,9 +17,9 @@ package org.traccar.api; import org.traccar.helper.Log; -import javax.ws.rs.WebApplicationException; -import javax.ws.rs.core.Response; -import javax.ws.rs.ext.ExceptionMapper; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; public class ResourceErrorHandler implements ExceptionMapper<Exception> { diff --git a/src/main/java/org/traccar/api/SimpleObjectResource.java b/src/main/java/org/traccar/api/SimpleObjectResource.java index 4a435ca7d..c9d41b063 100644 --- a/src/main/java/org/traccar/api/SimpleObjectResource.java +++ b/src/main/java/org/traccar/api/SimpleObjectResource.java @@ -23,8 +23,8 @@ 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 jakarta.ws.rs.GET; +import jakarta.ws.rs.QueryParam; import java.util.Collection; import java.util.LinkedList; diff --git a/src/main/java/org/traccar/api/resource/AttributeResource.java b/src/main/java/org/traccar/api/resource/AttributeResource.java index f85e90133..52c4d6324 100644 --- a/src/main/java/org/traccar/api/resource/AttributeResource.java +++ b/src/main/java/org/traccar/api/resource/AttributeResource.java @@ -16,17 +16,17 @@ */ package org.traccar.api.resource; -import javax.inject.Inject; -import javax.ws.rs.Consumes; -import javax.ws.rs.DELETE; -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.MediaType; -import javax.ws.rs.core.Response; +import jakarta.inject.Inject; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import org.traccar.api.ExtendedObjectResource; import org.traccar.model.Attribute; @@ -78,21 +78,21 @@ public class AttributeResource extends ExtendedObjectResource<Attribute> { } @POST - public Response add(Attribute entity) throws StorageException { + public Response add(Attribute entity) throws Exception { permissionsService.checkAdmin(getUserId()); return super.add(entity); } @Path("{id}") @PUT - public Response update(Attribute entity) throws StorageException { + public Response update(Attribute entity) throws Exception { permissionsService.checkAdmin(getUserId()); return super.update(entity); } @Path("{id}") @DELETE - public Response remove(@PathParam("id") long id) throws StorageException { + public Response remove(@PathParam("id") long id) throws Exception { permissionsService.checkAdmin(getUserId()); return super.remove(id); } diff --git a/src/main/java/org/traccar/api/resource/CalendarResource.java b/src/main/java/org/traccar/api/resource/CalendarResource.java index 9399c34a5..f6c1f3c59 100644 --- a/src/main/java/org/traccar/api/resource/CalendarResource.java +++ b/src/main/java/org/traccar/api/resource/CalendarResource.java @@ -16,10 +16,10 @@ */ package org.traccar.api.resource; -import javax.ws.rs.Consumes; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.core.MediaType; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; import org.traccar.api.SimpleObjectResource; import org.traccar.model.Calendar; diff --git a/src/main/java/org/traccar/api/resource/CommandResource.java b/src/main/java/org/traccar/api/resource/CommandResource.java index 3460cf6e0..c23d91e77 100644 --- a/src/main/java/org/traccar/api/resource/CommandResource.java +++ b/src/main/java/org/traccar/api/resource/CommandResource.java @@ -23,9 +23,12 @@ import org.traccar.BaseProtocol; import org.traccar.ServerManager; import org.traccar.api.ExtendedObjectResource; import org.traccar.database.CommandsManager; +import org.traccar.helper.model.DeviceUtil; import org.traccar.model.Command; import org.traccar.model.Device; +import org.traccar.model.Group; import org.traccar.model.Position; +import org.traccar.model.QueuedCommand; import org.traccar.model.Typed; import org.traccar.model.User; import org.traccar.model.UserRestrictions; @@ -34,15 +37,15 @@ 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.POST; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.QueryParam; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; +import jakarta.inject.Inject; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.ArrayList; @@ -104,7 +107,7 @@ public class CommandResource extends ExtendedObjectResource<Command> { @POST @Path("send") - public Response send(Command entity) throws Exception { + public Response send(Command entity, @QueryParam("groupId") long groupId) throws Exception { if (entity.getId() > 0) { permissionsService.checkPermission(baseClass, getUserId(), entity.getId()); long deviceId = entity.getDeviceId(); @@ -114,9 +117,28 @@ public class CommandResource extends ExtendedObjectResource<Command> { } else { permissionsService.checkRestriction(getUserId(), UserRestrictions::getLimitCommands); } - permissionsService.checkPermission(Device.class, getUserId(), entity.getDeviceId()); - if (!commandsManager.sendCommand(entity)) { - return Response.accepted(entity).build(); + + if (groupId > 0) { + permissionsService.checkPermission(Group.class, getUserId(), groupId); + var devices = DeviceUtil.getAccessibleDevices(storage, getUserId(), List.of(), List.of(groupId)); + List<QueuedCommand> queuedCommands = new ArrayList<>(); + for (Device device : devices) { + Command command = QueuedCommand.fromCommand(entity).toCommand(); + command.setDeviceId(device.getId()); + QueuedCommand queuedCommand = commandsManager.sendCommand(command); + if (queuedCommand != null) { + queuedCommands.add(queuedCommand); + } + } + if (!queuedCommands.isEmpty()) { + return Response.accepted(queuedCommands).build(); + } + } else { + permissionsService.checkPermission(Device.class, getUserId(), entity.getDeviceId()); + QueuedCommand queuedCommand = commandsManager.sendCommand(entity); + if (queuedCommand != null) { + return Response.accepted(queuedCommand).build(); + } } return Response.ok(entity).build(); } diff --git a/src/main/java/org/traccar/api/resource/DeviceResource.java b/src/main/java/org/traccar/api/resource/DeviceResource.java index c0b0cea0d..89bba7237 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 - 2022 Anton Tananaev (anton@traccar.org) + * Copyright 2015 - 2024 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,17 @@ */ package org.traccar.api.resource; +import jakarta.ws.rs.FormParam; import org.traccar.api.BaseObjectResource; +import org.traccar.api.signature.TokenManager; import org.traccar.broadcast.BroadcastService; +import org.traccar.config.Config; +import org.traccar.config.Keys; import org.traccar.database.MediaManager; import org.traccar.helper.LogAction; import org.traccar.model.Device; import org.traccar.model.DeviceAccumulators; +import org.traccar.model.Permission; import org.traccar.model.Position; import org.traccar.model.User; import org.traccar.session.ConnectionManager; @@ -30,23 +35,25 @@ 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 jakarta.inject.Inject; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.HeaderParam; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import java.io.File; import java.io.FileInputStream; import java.io.IOException; +import java.security.GeneralSecurityException; import java.util.Collection; +import java.util.Date; import java.util.LinkedList; import java.util.List; @@ -56,6 +63,9 @@ import java.util.List; public class DeviceResource extends BaseObjectResource<Device> { @Inject + private Config config; + + @Inject private CacheManager cacheManager; @Inject @@ -67,6 +77,9 @@ public class DeviceResource extends BaseObjectResource<Device> { @Inject private MediaManager mediaManager; + @Inject + private TokenManager tokenManager; + public DeviceResource() { super(Device.class); } @@ -120,7 +133,7 @@ public class DeviceResource extends BaseObjectResource<Device> { @Path("{id}/accumulators") @PUT - public Response updateAccumulators(DeviceAccumulators entity) throws StorageException { + public Response updateAccumulators(DeviceAccumulators entity) throws Exception { if (permissionsService.notAdmin(getUserId())) { permissionsService.checkManager(getUserId()); permissionsService.checkPermission(Device.class, getUserId(), entity.getDeviceId()); @@ -183,4 +196,50 @@ public class DeviceResource extends BaseObjectResource<Device> { return Response.status(Response.Status.NOT_FOUND).build(); } + @Path("share") + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + @POST + public String shareDevice( + @FormParam("deviceId") long deviceId, + @FormParam("expiration") Date expiration) throws StorageException, GeneralSecurityException, IOException { + + User user = permissionsService.getUser(getUserId()); + if (permissionsService.getServer().getBoolean(Keys.DEVICE_SHARE_DISABLE.getKey())) { + throw new SecurityException("Sharing is disabled"); + } + if (user.getTemporary()) { + throw new SecurityException("Temporary user"); + } + if (user.getExpirationTime() != null && user.getExpirationTime().before(expiration)) { + expiration = user.getExpirationTime(); + } + + Device device = storage.getObject(Device.class, new Request( + new Columns.All(), + new Condition.And( + new Condition.Equals("id", deviceId), + new Condition.Permission(User.class, user.getId(), Device.class)))); + + String shareEmail = user.getEmail() + ":" + device.getUniqueId(); + User share = storage.getObject(User.class, new Request( + new Columns.All(), new Condition.Equals("email", shareEmail))); + + if (share == null) { + share = new User(); + share.setName(device.getName()); + share.setEmail(shareEmail); + share.setExpirationTime(expiration); + share.setTemporary(true); + share.setReadonly(true); + share.setLimitCommands(user.getLimitCommands() || !config.getBoolean(Keys.WEB_SHARE_DEVICE_COMMANDS)); + share.setDisableReports(user.getDisableReports() || !config.getBoolean(Keys.WEB_SHARE_DEVICE_REPORTS)); + + share.setId(storage.addObject(share, new Request(new Columns.Exclude("id")))); + + storage.addPermission(new Permission(User.class, share.getId(), Device.class, deviceId)); + } + + return tokenManager.generateToken(share.getId(), expiration); + } + } diff --git a/src/main/java/org/traccar/api/resource/DriverResource.java b/src/main/java/org/traccar/api/resource/DriverResource.java index 91aa54c5e..19cf74f39 100644 --- a/src/main/java/org/traccar/api/resource/DriverResource.java +++ b/src/main/java/org/traccar/api/resource/DriverResource.java @@ -16,10 +16,10 @@ */ package org.traccar.api.resource; -import javax.ws.rs.Consumes; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.core.MediaType; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; import org.traccar.api.ExtendedObjectResource; import org.traccar.model.Driver; diff --git a/src/main/java/org/traccar/api/resource/EventResource.java b/src/main/java/org/traccar/api/resource/EventResource.java index afdaf52b5..1f20b880d 100644 --- a/src/main/java/org/traccar/api/resource/EventResource.java +++ b/src/main/java/org/traccar/api/resource/EventResource.java @@ -23,14 +23,14 @@ 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; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.Produces; -import javax.ws.rs.WebApplicationException; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; @Path("events") @Produces(MediaType.APPLICATION_JSON) diff --git a/src/main/java/org/traccar/api/resource/GeofenceResource.java b/src/main/java/org/traccar/api/resource/GeofenceResource.java index 58f2c188c..030690889 100644 --- a/src/main/java/org/traccar/api/resource/GeofenceResource.java +++ b/src/main/java/org/traccar/api/resource/GeofenceResource.java @@ -18,10 +18,10 @@ package org.traccar.api.resource; import org.traccar.api.ExtendedObjectResource; import org.traccar.model.Geofence; -import javax.ws.rs.Consumes; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.core.MediaType; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; @Path("geofences") @Produces(MediaType.APPLICATION_JSON) diff --git a/src/main/java/org/traccar/api/resource/GroupResource.java b/src/main/java/org/traccar/api/resource/GroupResource.java index fcea15d0a..628f8f655 100644 --- a/src/main/java/org/traccar/api/resource/GroupResource.java +++ b/src/main/java/org/traccar/api/resource/GroupResource.java @@ -18,10 +18,10 @@ package org.traccar.api.resource; import org.traccar.api.SimpleObjectResource; import org.traccar.model.Group; -import javax.ws.rs.Consumes; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.core.MediaType; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; @Path("groups") @Produces(MediaType.APPLICATION_JSON) diff --git a/src/main/java/org/traccar/api/resource/MaintenanceResource.java b/src/main/java/org/traccar/api/resource/MaintenanceResource.java index fa1b359ce..12841e497 100644 --- a/src/main/java/org/traccar/api/resource/MaintenanceResource.java +++ b/src/main/java/org/traccar/api/resource/MaintenanceResource.java @@ -16,10 +16,10 @@ */ package org.traccar.api.resource; -import javax.ws.rs.Consumes; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.core.MediaType; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; import org.traccar.api.ExtendedObjectResource; import org.traccar.model.Maintenance; diff --git a/src/main/java/org/traccar/api/resource/NotificationResource.java b/src/main/java/org/traccar/api/resource/NotificationResource.java index 2e4ad12f3..43dc1fa98 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 - 2022 Anton Tananaev (anton@traccar.org) + * Copyright 2016 - 2023 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,6 +15,16 @@ */ package org.traccar.api.resource; +import jakarta.inject.Inject; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.traccar.api.ExtendedObjectResource; @@ -23,20 +33,16 @@ import org.traccar.model.Notification; import org.traccar.model.Typed; import org.traccar.model.User; import org.traccar.notification.MessageException; +import org.traccar.notification.NotificationMessage; import org.traccar.notification.NotificatorManager; 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.POST; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.Produces; -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.LinkedList; import java.util.List; @@ -80,10 +86,10 @@ public class NotificationResource extends ExtendedObjectResource<Notification> { @POST @Path("test") - public Response testMessage() throws MessageException, InterruptedException, StorageException { + public Response testMessage() throws MessageException, StorageException { User user = permissionsService.getUser(getUserId()); for (Typed method : notificatorManager.getAllNotificatorTypes()) { - notificatorManager.getNotificator(method.getType()).send(user, new Event("test", 0), null); + notificatorManager.getNotificator(method.getType()).send(null, user, new Event("test", 0), null); } return Response.noContent().build(); } @@ -91,9 +97,31 @@ public class NotificationResource extends ExtendedObjectResource<Notification> { @POST @Path("test/{notificator}") public Response testMessage(@PathParam("notificator") String notificator) - throws MessageException, InterruptedException, StorageException { + throws MessageException, StorageException { User user = permissionsService.getUser(getUserId()); - notificatorManager.getNotificator(notificator).send(user, new Event("test", 0), null); + notificatorManager.getNotificator(notificator).send(null, user, new Event("test", 0), null); + return Response.noContent().build(); + } + + @POST + @Path("send/{notificator}") + public Response sendMessage( + @PathParam("notificator") String notificator, @QueryParam("userId") List<Long> userIds, + NotificationMessage message) throws MessageException, StorageException { + permissionsService.checkAdmin(getUserId()); + List<User> users; + if (userIds.isEmpty()) { + users = storage.getObjects(User.class, new Request(new Columns.All())); + } else { + users = new ArrayList<>(); + for (long userId : userIds) { + users.add(storage.getObject( + User.class, new Request(new Columns.All(), new Condition.Equals("id", userId)))); + } + } + for (User user : users) { + notificatorManager.getNotificator(notificator).send(user, message, null, null); + } return Response.noContent().build(); } diff --git a/src/main/java/org/traccar/api/resource/OrderResource.java b/src/main/java/org/traccar/api/resource/OrderResource.java index 77608a508..3852b975f 100644 --- a/src/main/java/org/traccar/api/resource/OrderResource.java +++ b/src/main/java/org/traccar/api/resource/OrderResource.java @@ -18,10 +18,10 @@ package org.traccar.api.resource; import org.traccar.api.SimpleObjectResource; import org.traccar.model.Order; -import javax.ws.rs.Consumes; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.core.MediaType; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; @Path("orders") @Produces(MediaType.APPLICATION_JSON) diff --git a/src/main/java/org/traccar/api/resource/PasswordResource.java b/src/main/java/org/traccar/api/resource/PasswordResource.java index 2d87a8665..22b3f0cd3 100644 --- a/src/main/java/org/traccar/api/resource/PasswordResource.java +++ b/src/main/java/org/traccar/api/resource/PasswordResource.java @@ -25,16 +25,16 @@ 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; -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; +import jakarta.annotation.security.PermitAll; +import jakarta.inject.Inject; +import jakarta.mail.MessagingException; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.FormParam; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import java.io.IOException; import java.security.GeneralSecurityException; @@ -63,7 +63,7 @@ public class PasswordResource extends BaseResource { if (user != null) { var velocityContext = textTemplateFormatter.prepareContext(permissionsService.getServer(), user); var fullMessage = textTemplateFormatter.formatMessage(velocityContext, "passwordReset", "full"); - mailManager.sendMessage(user, fullMessage.getSubject(), fullMessage.getBody()); + mailManager.sendMessage(user, true, fullMessage.getSubject(), fullMessage.getBody()); } return Response.ok().build(); } @@ -75,7 +75,7 @@ public class PasswordResource extends BaseResource { @FormParam("token") String token, @FormParam("password") String password) throws StorageException, GeneralSecurityException, IOException { - long userId = tokenManager.verifyToken(token); + long userId = tokenManager.verifyToken(token).getUserId(); User user = storage.getObject(User.class, new Request( new Columns.All(), new Condition.Equals("id", userId))); if (user != null) { diff --git a/src/main/java/org/traccar/api/resource/PermissionsResource.java b/src/main/java/org/traccar/api/resource/PermissionsResource.java index d35cb98bb..9e2d21f2c 100644 --- a/src/main/java/org/traccar/api/resource/PermissionsResource.java +++ b/src/main/java/org/traccar/api/resource/PermissionsResource.java @@ -23,15 +23,15 @@ 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; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.WebApplicationException; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; +import jakarta.inject.Inject; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; @@ -48,7 +48,7 @@ public class PermissionsResource extends BaseResource { 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()); + permissionsService.checkPermission(permission.getPropertyClass(), getUserId(), permission.getPropertyId()); } } @@ -64,7 +64,7 @@ public class PermissionsResource extends BaseResource { @Path("bulk") @POST - public Response add(List<LinkedHashMap<String, Long>> entities) throws StorageException, ClassNotFoundException { + public Response add(List<LinkedHashMap<String, Long>> entities) throws Exception { permissionsService.checkRestriction(getUserId(), UserRestrictions::getReadonly); checkPermissionTypes(entities); for (LinkedHashMap<String, Long> entity: entities) { @@ -74,7 +74,8 @@ public class PermissionsResource extends BaseResource { cacheManager.invalidatePermission( true, permission.getOwnerClass(), permission.getOwnerId(), - permission.getPropertyClass(), permission.getPropertyId()); + permission.getPropertyClass(), permission.getPropertyId(), + true); LogAction.link(getUserId(), permission.getOwnerClass(), permission.getOwnerId(), permission.getPropertyClass(), permission.getPropertyId()); @@ -83,13 +84,13 @@ public class PermissionsResource extends BaseResource { } @POST - public Response add(LinkedHashMap<String, Long> entity) throws StorageException, ClassNotFoundException { + public Response add(LinkedHashMap<String, Long> entity) throws Exception { return add(Collections.singletonList(entity)); } @DELETE @Path("bulk") - public Response remove(List<LinkedHashMap<String, Long>> entities) throws StorageException, ClassNotFoundException { + public Response remove(List<LinkedHashMap<String, Long>> entities) throws Exception { permissionsService.checkRestriction(getUserId(), UserRestrictions::getReadonly); checkPermissionTypes(entities); for (LinkedHashMap<String, Long> entity: entities) { @@ -99,7 +100,8 @@ public class PermissionsResource extends BaseResource { cacheManager.invalidatePermission( true, permission.getOwnerClass(), permission.getOwnerId(), - permission.getPropertyClass(), permission.getPropertyId()); + permission.getPropertyClass(), permission.getPropertyId(), + false); LogAction.unlink(getUserId(), permission.getOwnerClass(), permission.getOwnerId(), permission.getPropertyClass(), permission.getPropertyId()); @@ -108,7 +110,7 @@ public class PermissionsResource extends BaseResource { } @DELETE - public Response remove(LinkedHashMap<String, Long> entity) throws StorageException, ClassNotFoundException { + public Response remove(LinkedHashMap<String, Long> entity) throws Exception { 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 042dd1e23..0d783a0fe 100644 --- a/src/main/java/org/traccar/api/resource/PositionResource.java +++ b/src/main/java/org/traccar/api/resource/PositionResource.java @@ -28,21 +28,23 @@ 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 javax.ws.rs.core.Response; -import javax.ws.rs.core.StreamingOutput; +import jakarta.inject.Inject; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.StreamingOutput; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.List; +import java.util.LinkedList; @Path("positions") @Produces(MediaType.APPLICATION_JSON) @@ -86,6 +88,21 @@ public class PositionResource extends BaseResource { } } + @DELETE + public Response remove( + @QueryParam("deviceId") long deviceId, + @QueryParam("from") Date from, @QueryParam("to") Date to) throws StorageException { + permissionsService.checkPermission(Device.class, getUserId(), deviceId); + permissionsService.checkRestriction(getUserId(), UserRestrictions::getReadonly); + + var conditions = new LinkedList<Condition>(); + conditions.add(new Condition.Equals("deviceId", deviceId)); + conditions.add(new Condition.Between("fixTime", "from", from, "to", to)); + storage.removeObject(Position.class, new Request(Condition.merge(conditions))); + + return Response.status(Response.Status.NO_CONTENT).build(); + } + @Path("kml") @GET @Produces("application/vnd.google-earth.kml+xml") diff --git a/src/main/java/org/traccar/api/resource/ReportResource.java b/src/main/java/org/traccar/api/resource/ReportResource.java index 6944de9cb..55a96fa90 100644 --- a/src/main/java/org/traccar/api/resource/ReportResource.java +++ b/src/main/java/org/traccar/api/resource/ReportResource.java @@ -16,13 +16,14 @@ */ package org.traccar.api.resource; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.traccar.api.BaseResource; +import org.traccar.api.SimpleObjectResource; import org.traccar.helper.LogAction; import org.traccar.model.Event; import org.traccar.model.Position; +import org.traccar.model.Report; import org.traccar.model.UserRestrictions; +import org.traccar.reports.CombinedReportProvider; +import org.traccar.reports.DevicesReportProvider; import org.traccar.reports.EventsReportProvider; import org.traccar.reports.RouteReportProvider; import org.traccar.reports.StopsReportProvider; @@ -30,23 +31,24 @@ import org.traccar.reports.SummaryReportProvider; import org.traccar.reports.TripsReportProvider; import org.traccar.reports.common.ReportExecutor; import org.traccar.reports.common.ReportMailer; +import org.traccar.reports.model.CombinedReportItem; 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.inject.Inject; -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 javax.ws.rs.core.StreamingOutput; +import jakarta.inject.Inject; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.StreamingOutput; import java.util.Collection; import java.util.Date; import java.util.List; @@ -54,13 +56,14 @@ import java.util.List; @Path("reports") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) -public class ReportResource extends BaseResource { - - private static final Logger LOGGER = LoggerFactory.getLogger(ReportResource.class); +public class ReportResource extends SimpleObjectResource<Report> { private static final String EXCEL = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; @Inject + private CombinedReportProvider combinedReportProvider; + + @Inject private EventsReportProvider eventsReportProvider; @Inject @@ -76,8 +79,15 @@ public class ReportResource extends BaseResource { private TripsReportProvider tripsReportProvider; @Inject + private DevicesReportProvider devicesReportProvider; + + @Inject private ReportMailer reportMailer; + public ReportResource() { + super(Report.class); + } + private Response executeReport(long userId, boolean mail, ReportExecutor executor) { if (mail) { reportMailer.sendAsync(userId, executor); @@ -95,6 +105,18 @@ public class ReportResource extends BaseResource { } } + @Path("combined") + @GET + public Collection<CombinedReportItem> getCombined( + @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(), "combined", from, to, deviceIds, groupIds); + return combinedReportProvider.getObjects(getUserId(), deviceIds, groupIds, from, to); + } + @Path("route") @GET public Collection<Position> getRoute( @@ -301,4 +323,15 @@ public class ReportResource extends BaseResource { return getStopsExcel(deviceIds, groupIds, from, to, type.equals("mail")); } + @Path("devices/{type:xlsx|mail}") + @GET + @Produces(EXCEL) + public Response geDevicesExcel( + @PathParam("type") String type) throws StorageException { + permissionsService.checkRestriction(getUserId(), UserRestrictions::getDisableReports); + return executeReport(getUserId(), type.equals("mail"), stream -> { + devicesReportProvider.getExcel(stream, getUserId()); + }); + } + } diff --git a/src/main/java/org/traccar/api/resource/ServerResource.java b/src/main/java/org/traccar/api/resource/ServerResource.java index 4b7ee9189..2a72d2773 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 - 2022 Anton Tananaev (anton@traccar.org) + * Copyright 2015 - 2023 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,30 +16,43 @@ package org.traccar.api.resource; import org.traccar.api.BaseResource; -import org.traccar.helper.model.UserUtil; -import org.traccar.mail.MailManager; +import org.traccar.model.ObjectOperation; +import org.traccar.config.Config; +import org.traccar.config.Keys; +import org.traccar.database.OpenIdProvider; import org.traccar.geocoder.Geocoder; import org.traccar.helper.Log; import org.traccar.helper.LogAction; +import org.traccar.helper.model.UserUtil; +import org.traccar.mail.MailManager; import org.traccar.model.Server; import org.traccar.model.User; import org.traccar.session.cache.CacheManager; +import org.traccar.sms.SmsManager; 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; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.QueryParam; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; +import jakarta.annotation.Nullable; +import jakarta.annotation.security.PermitAll; +import jakarta.inject.Inject; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; import java.util.Arrays; import java.util.Collection; import java.util.TimeZone; @@ -50,6 +63,9 @@ import java.util.TimeZone; public class ServerResource extends BaseResource { @Inject + private Config config; + + @Inject private CacheManager cacheManager; @Inject @@ -57,6 +73,14 @@ public class ServerResource extends BaseResource { @Inject @Nullable + private SmsManager smsManager; + + @Inject + @Nullable + private OpenIdProvider openIdProvider; + + @Inject + @Nullable private Geocoder geocoder; @PermitAll @@ -64,7 +88,10 @@ public class ServerResource extends BaseResource { public Server get() throws StorageException { Server server = storage.getObject(Server.class, new Request(new Columns.All())); server.setEmailEnabled(mailManager.getEmailEnabled()); + server.setTextEnabled(smsManager != null); server.setGeocoderEnabled(geocoder != null); + server.setOpenIdEnabled(openIdProvider != null); + server.setOpenIdForce(openIdProvider != null && openIdProvider.getForce()); User user = permissionsService.getUser(getUserId()); if (user != null) { if (user.getAdministrator()) { @@ -73,21 +100,18 @@ public class ServerResource extends BaseResource { } else { server.setNewServer(UserUtil.isEmpty(storage)); } - if (user != null && user.getAdministrator()) { - server.setStorageSpace(Log.getStorageSpace()); - } return server; } @PUT - public Response update(Server entity) throws StorageException { + public Response update(Server server) throws Exception { permissionsService.checkAdmin(getUserId()); - storage.updateObject(entity, new Request( + storage.updateObject(server, 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(); + new Condition.Equals("id", server.getId()))); + cacheManager.invalidateObject(true, Server.class, server.getId(), ObjectOperation.UPDATE); + LogAction.edit(getUserId(), server); + return Response.ok(server).build(); } @Path("geocode") @@ -106,4 +130,35 @@ public class ServerResource extends BaseResource { return Arrays.asList(TimeZone.getAvailableIDs()); } + @Path("file/{path}") + @POST + @Consumes("*/*") + public Response uploadFile(@PathParam("path") String path, File inputFile) throws IOException, StorageException { + permissionsService.checkAdmin(getUserId()); + String root = config.getString(Keys.WEB_OVERRIDE, config.getString(Keys.WEB_PATH)); + + var rootPath = Paths.get(root).normalize(); + var outputPath = rootPath.resolve(path).normalize(); + if (!outputPath.startsWith(rootPath)) { + return Response.status(Response.Status.BAD_REQUEST).build(); + } + + var directoryPath = outputPath.getParent(); + if (directoryPath != null) { + Files.createDirectories(directoryPath); + } + + try (var input = new FileInputStream(inputFile); var output = new FileOutputStream(outputPath.toFile())) { + input.transferTo(output); + } + return Response.ok().build(); + } + + @Path("cache") + @GET + public String cache() throws StorageException { + permissionsService.checkAdmin(getUserId()); + return cacheManager.toString(); + } + } diff --git a/src/main/java/org/traccar/api/resource/SessionResource.java b/src/main/java/org/traccar/api/resource/SessionResource.java index 7025d5fa7..979b62589 100644 --- a/src/main/java/org/traccar/api/resource/SessionResource.java +++ b/src/main/java/org/traccar/api/resource/SessionResource.java @@ -16,39 +16,41 @@ package org.traccar.api.resource; import org.traccar.api.BaseResource; +import org.traccar.api.security.CodeRequiredException; +import org.traccar.api.security.LoginResult; import org.traccar.api.security.LoginService; import org.traccar.api.signature.TokenManager; -import org.traccar.helper.DataConverter; +import org.traccar.database.OpenIdProvider; import org.traccar.helper.LogAction; -import org.traccar.helper.ServletHelper; +import org.traccar.helper.WebHelper; 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; -import javax.ws.rs.DELETE; -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 com.nimbusds.oauth2.sdk.ParseException; +import jakarta.annotation.Nullable; +import jakarta.annotation.security.PermitAll; +import jakarta.inject.Inject; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.FormParam; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import java.io.IOException; -import java.net.URLDecoder; -import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; import java.util.Date; +import java.net.URI; @Path("session") @Produces(MediaType.APPLICATION_JSON) @@ -56,13 +58,16 @@ import java.util.Date; public class SessionResource extends BaseResource { public static final String USER_ID_KEY = "userId"; - public static final String USER_COOKIE_KEY = "user"; - public static final String PASS_COOKIE_KEY = "password"; + public static final String EXPIRATION_KEY = "expiration"; @Inject private LoginService loginService; @Inject + @Nullable + private OpenIdProvider openIdProvider; + + @Inject private TokenManager tokenManager; @Context @@ -73,48 +78,22 @@ public class SessionResource extends BaseResource { public User get(@QueryParam("token") String token) throws StorageException, IOException, GeneralSecurityException { if (token != null) { - User user = loginService.login(token); - if (user != null) { + LoginResult loginResult = loginService.login(token); + if (loginResult != null) { + User user = loginResult.getUser(); request.getSession().setAttribute(USER_ID_KEY, user.getId()); - LogAction.login(user.getId(), ServletHelper.retrieveRemoteAddress(request)); + request.getSession().setAttribute(EXPIRATION_KEY, loginResult.getExpiration()); + LogAction.login(user.getId(), WebHelper.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)); - 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)); - password = new String(passwordBytes, StandardCharsets.UTF_8); - } - } - } - if (email != null && password != null) { - User user = loginService.login(email, password); - if (user != null) { - request.getSession().setAttribute(USER_ID_KEY, user.getId()); - LogAction.login(user.getId(), ServletHelper.retrieveRemoteAddress(request)); - return user; - } - } - - } else { - + if (userId != null) { User user = permissionsService.getUser(userId); if (user != null) { return user; } - } throw new WebApplicationException(Response.status(Response.Status.NOT_FOUND).build()); @@ -123,32 +102,44 @@ public class SessionResource extends BaseResource { @Path("{id}") @GET public User get(@PathParam("id") long userId) throws StorageException { - permissionsService.checkAdmin(getUserId()); + permissionsService.checkUser(getUserId(), userId); 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)); + LogAction.login(user.getId(), WebHelper.retrieveRemoteAddress(request)); return user; } @PermitAll @POST public User add( - @FormParam("email") String email, @FormParam("password") String password) throws StorageException { - User user = loginService.login(email, password); - if (user != null) { + @FormParam("email") String email, + @FormParam("password") String password, + @FormParam("code") Integer code) throws StorageException { + LoginResult loginResult; + try { + loginResult = loginService.login(email, password, code); + } catch (CodeRequiredException e) { + Response response = Response + .status(Response.Status.UNAUTHORIZED) + .header("WWW-Authenticate", "TOTP") + .build(); + throw new WebApplicationException(response); + } + if (loginResult != null) { + User user = loginResult.getUser(); request.getSession().setAttribute(USER_ID_KEY, user.getId()); - LogAction.login(user.getId(), ServletHelper.retrieveRemoteAddress(request)); + LogAction.login(user.getId(), WebHelper.retrieveRemoteAddress(request)); return user; } else { - LogAction.failedLogin(ServletHelper.retrieveRemoteAddress(request)); + LogAction.failedLogin(WebHelper.retrieveRemoteAddress(request)); throw new WebApplicationException(Response.status(Response.Status.UNAUTHORIZED).build()); } } @DELETE public Response remove() { - LogAction.logout(getUserId(), ServletHelper.retrieveRemoteAddress(request)); + LogAction.logout(getUserId(), WebHelper.retrieveRemoteAddress(request)); request.getSession().removeAttribute(USER_ID_KEY); return Response.noContent().build(); } @@ -157,7 +148,28 @@ public class SessionResource extends BaseResource { @POST public String requestToken( @FormParam("expiration") Date expiration) throws StorageException, GeneralSecurityException, IOException { + Date currentExpiration = (Date) request.getSession().getAttribute(EXPIRATION_KEY); + if (currentExpiration != null && currentExpiration.before(expiration)) { + expiration = currentExpiration; + } return tokenManager.generateToken(getUserId(), expiration); } + @PermitAll + @Path("openid/auth") + @GET + public Response openIdAuth() { + return Response.seeOther(openIdProvider.createAuthUri()).build(); + } + + @PermitAll + @Path("openid/callback") + @GET + public Response requestToken() throws IOException, StorageException, ParseException, GeneralSecurityException { + StringBuilder requestUrl = new StringBuilder(request.getRequestURL().toString()); + String queryString = request.getQueryString(); + String requestUri = requestUrl.append('?').append(queryString).toString(); + + return Response.seeOther(openIdProvider.handleCallback(URI.create(requestUri), request)).build(); + } } diff --git a/src/main/java/org/traccar/api/resource/StatisticsResource.java b/src/main/java/org/traccar/api/resource/StatisticsResource.java index 1f2296f28..0c728c77d 100644 --- a/src/main/java/org/traccar/api/resource/StatisticsResource.java +++ b/src/main/java/org/traccar/api/resource/StatisticsResource.java @@ -23,12 +23,12 @@ 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; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.QueryParam; -import javax.ws.rs.core.MediaType; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; import java.util.Collection; import java.util.Date; diff --git a/src/main/java/org/traccar/api/resource/UserResource.java b/src/main/java/org/traccar/api/resource/UserResource.java index e41ebbe61..47ea9b07c 100644 --- a/src/main/java/org/traccar/api/resource/UserResource.java +++ b/src/main/java/org/traccar/api/resource/UserResource.java @@ -15,6 +15,11 @@ */ package org.traccar.api.resource; +import com.warrenstrange.googleauth.GoogleAuthenticator; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.core.Context; import org.traccar.api.BaseObjectResource; import org.traccar.config.Config; import org.traccar.config.Keys; @@ -28,18 +33,17 @@ 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; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.QueryParam; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; +import jakarta.annotation.security.PermitAll; +import jakarta.inject.Inject; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import java.util.Collection; -import java.util.Date; @Path("users") @Produces(MediaType.APPLICATION_JSON) @@ -49,6 +53,9 @@ public class UserResource extends BaseObjectResource<User> { @Inject private Config config; + @Context + private HttpServletRequest request; + public UserResource() { super(User.class); } @@ -91,11 +98,11 @@ public class UserResource extends BaseObjectResource<User> { 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() + expirationDays * 86400000L)); + if (permissionsService.getServer().getBoolean(Keys.WEB_TOTP_FORCE.getKey()) + && entity.getTotpKey() == null) { + throw new SecurityException("One-time password key is required"); } + UserUtil.setUserDefaults(entity, config); } } @@ -117,4 +124,24 @@ public class UserResource extends BaseObjectResource<User> { return Response.ok(entity).build(); } + @Path("{id}") + @DELETE + public Response remove(@PathParam("id") long id) throws Exception { + Response response = super.remove(id); + if (getUserId() == id) { + request.getSession().removeAttribute(SessionResource.USER_ID_KEY); + } + return response; + } + + @Path("totp") + @PermitAll + @POST + public String generateTotpKey() throws StorageException { + if (!permissionsService.getServer().getBoolean(Keys.WEB_TOTP_ENABLE.getKey())) { + throw new SecurityException("One-time password is disabled"); + } + return new GoogleAuthenticator().createCredentials().getKey(); + } + } diff --git a/src/main/java/org/traccar/api/security/CodeRequiredException.java b/src/main/java/org/traccar/api/security/CodeRequiredException.java new file mode 100644 index 000000000..d522c6540 --- /dev/null +++ b/src/main/java/org/traccar/api/security/CodeRequiredException.java @@ -0,0 +1,22 @@ +/* + * Copyright 2023 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; + +public class CodeRequiredException extends SecurityException { + public CodeRequiredException() { + super("Code not provided"); + } +} diff --git a/src/main/java/org/traccar/api/security/LoginResult.java b/src/main/java/org/traccar/api/security/LoginResult.java new file mode 100644 index 000000000..1fccc36d1 --- /dev/null +++ b/src/main/java/org/traccar/api/security/LoginResult.java @@ -0,0 +1,29 @@ +package org.traccar.api.security; + +import org.traccar.model.User; + +import java.util.Date; + +public class LoginResult { + + private final User user; + private final Date expiration; + + public LoginResult(User user) { + this(user, null); + } + + public LoginResult(User user, Date expiration) { + this.user = user; + this.expiration = expiration; + } + + public User getUser() { + return user; + } + + public Date getExpiration() { + return expiration; + } + +} diff --git a/src/main/java/org/traccar/api/security/LoginService.java b/src/main/java/org/traccar/api/security/LoginService.java index 88bafcfb5..930c4fa46 100644 --- a/src/main/java/org/traccar/api/security/LoginService.java +++ b/src/main/java/org/traccar/api/security/LoginService.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 Anton Tananaev (anton@traccar.org) + * Copyright 2022 - 2023 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,10 +15,12 @@ */ package org.traccar.api.security; +import com.warrenstrange.googleauth.GoogleAuthenticator; import org.traccar.api.signature.TokenManager; import org.traccar.config.Config; import org.traccar.config.Keys; import org.traccar.database.LdapProvider; +import org.traccar.helper.model.UserUtil; import org.traccar.model.User; import org.traccar.storage.Storage; import org.traccar.storage.StorageException; @@ -26,46 +28,54 @@ 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 jakarta.annotation.Nullable; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; import java.io.IOException; import java.security.GeneralSecurityException; @Singleton public class LoginService { + private final Config config; private final Storage storage; private final TokenManager tokenManager; private final LdapProvider ldapProvider; private final String serviceAccountToken; private final boolean forceLdap; + private final boolean forceOpenId; @Inject public LoginService( Config config, Storage storage, TokenManager tokenManager, @Nullable LdapProvider ldapProvider) { this.storage = storage; + this.config = config; this.tokenManager = tokenManager; this.ldapProvider = ldapProvider; serviceAccountToken = config.getString(Keys.WEB_SERVICE_ACCOUNT_TOKEN); forceLdap = config.getBoolean(Keys.LDAP_FORCE); + forceOpenId = config.getBoolean(Keys.OPENID_FORCE); } - public User login(String token) throws StorageException, GeneralSecurityException, IOException { + public LoginResult login(String token) throws StorageException, GeneralSecurityException, IOException { if (serviceAccountToken != null && serviceAccountToken.equals(token)) { - return new ServiceAccountUser(); + return new LoginResult(new ServiceAccountUser()); } - long userId = tokenManager.verifyToken(token); + TokenManager.TokenData tokenData = tokenManager.verifyToken(token); User user = storage.getObject(User.class, new Request( - new Columns.All(), new Condition.Equals("id", userId))); + new Columns.All(), new Condition.Equals("id", tokenData.getUserId()))); if (user != null) { checkUserEnabled(user); } - return user; + return new LoginResult(user, tokenData.getExpiration()); } - public User login(String email, String password) throws StorageException { + public LoginResult login(String email, String password, Integer code) throws StorageException { + if (forceOpenId) { + return null; + } + email = email.trim(); User user = storage.getObject(User.class, new Request( new Columns.All(), @@ -75,20 +85,39 @@ public class LoginService { if (user != null) { if (ldapProvider != null && user.getLogin() != null && ldapProvider.login(user.getLogin(), password) || !forceLdap && user.isPasswordValid(password)) { + checkUserCode(user, code); checkUserEnabled(user); - return user; + return new LoginResult(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 new LoginResult(user); } } return null; } + public LoginResult login(String email, String name, boolean administrator) throws StorageException { + User user = storage.getObject(User.class, new Request( + new Columns.All(), + new Condition.Equals("email", email))); + + if (user == null) { + user = new User(); + UserUtil.setUserDefaults(user, config); + user.setName(name); + user.setEmail(email); + user.setFixedEmail(true); + user.setAdministrator(administrator); + user.setId(storage.addObject(user, new Request(new Columns.Exclude("id")))); + } + checkUserEnabled(user); + return new LoginResult(user); + } + private void checkUserEnabled(User user) throws SecurityException { if (user == null) { throw new SecurityException("Unknown account"); @@ -96,4 +125,17 @@ public class LoginService { user.checkDisabled(); } + private void checkUserCode(User user, Integer code) throws SecurityException { + String key = user.getTotpKey(); + if (key != null && !key.isEmpty()) { + if (code == null) { + throw new CodeRequiredException(); + } + GoogleAuthenticator authenticator = new GoogleAuthenticator(); + if (!authenticator.authorize(key, code)) { + throw new SecurityException("User authorization failed"); + } + } + } + } diff --git a/src/main/java/org/traccar/api/security/PermissionsService.java b/src/main/java/org/traccar/api/security/PermissionsService.java index 4421572d7..d60bbafb8 100644 --- a/src/main/java/org/traccar/api/security/PermissionsService.java +++ b/src/main/java/org/traccar/api/security/PermissionsService.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 Anton Tananaev (anton@traccar.org) + * Copyright 2022 - 2023 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. @@ -23,7 +23,8 @@ 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.Notification; +import org.traccar.model.Schedulable; import org.traccar.model.Server; import org.traccar.model.User; import org.traccar.model.UserRestrictions; @@ -33,7 +34,7 @@ import org.traccar.storage.query.Columns; import org.traccar.storage.query.Condition; import org.traccar.storage.query.Request; -import javax.inject.Inject; +import jakarta.inject.Inject; import java.util.Objects; @RequestScoped @@ -129,17 +130,17 @@ public class PermissionsService { GroupedModel before = null; if (!addition) { before = storage.getObject(after.getClass(), new Request( - new Columns.Include("groupId"), new Condition.Equals("id", object.getId()))); + new Columns.Include("groupId"), new Condition.Equals("id", after.getId()))); } if (before == null || before.getGroupId() != after.getGroupId()) { checkPermission(Group.class, userId, after.getGroupId()); } } } - if (object instanceof ScheduledModel) { - ScheduledModel after = ((ScheduledModel) object); + if (object instanceof Schedulable) { + Schedulable after = ((Schedulable) object); if (after.getCalendarId() > 0) { - ScheduledModel before = null; + Schedulable before = null; if (!addition) { before = storage.getObject(after.getClass(), new Request( new Columns.Include("calendarId"), new Condition.Equals("id", object.getId()))); @@ -149,6 +150,19 @@ public class PermissionsService { } } } + if (object instanceof Notification) { + Notification after = ((Notification) object); + if (after.getCommandId() > 0) { + Notification before = null; + if (!addition) { + before = storage.getObject(after.getClass(), new Request( + new Columns.Include("commandId"), new Condition.Equals("id", object.getId()))); + } + if (before == null || before.getCommandId() != after.getCommandId()) { + checkPermission(Command.class, userId, after.getCommandId()); + } + } + } } } @@ -167,7 +181,7 @@ public class PermissionsService { || before.getUserLimit() != after.getUserLimit()) { checkAdmin(userId); } - User user = getUser(userId); + User user = userId > 0 ? getUser(userId) : null; if (user != null && user.getExpirationTime() != null && !Objects.equals(before.getExpirationTime(), after.getExpirationTime()) && (after.getExpirationTime() == null diff --git a/src/main/java/org/traccar/api/security/SecurityRequestFilter.java b/src/main/java/org/traccar/api/security/SecurityRequestFilter.java index 94b6bbf05..12a5dbecf 100644 --- a/src/main/java/org/traccar/api/security/SecurityRequestFilter.java +++ b/src/main/java/org/traccar/api/security/SecurityRequestFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 - 2022 Anton Tananaev (anton@traccar.org) + * Copyright 2015 - 2023 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,6 +15,7 @@ */ package org.traccar.api.security; +import com.google.inject.Injector; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.traccar.api.resource.SessionResource; @@ -23,32 +24,26 @@ 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 jakarta.annotation.security.PermitAll; +import jakarta.inject.Inject; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.ContainerRequestFilter; +import jakarta.ws.rs.container.ResourceInfo; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.SecurityContext; import java.io.IOException; import java.lang.reflect.Method; import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; +import java.util.Date; public class SecurityRequestFilter implements ContainerRequestFilter { private static final Logger LOGGER = LoggerFactory.getLogger(SecurityRequestFilter.class); - 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"; - public static String[] decodeBasicAuth(String auth) { auth = auth.replaceFirst("[B|b]asic ", ""); byte[] decodedBytes = DataConverter.parseBase64(auth); @@ -70,6 +65,9 @@ public class SecurityRequestFilter implements ContainerRequestFilter { @Inject private StatisticsManager statisticsManager; + @Inject + private Injector injector; + @Override public void filter(ContainerRequestContext requestContext) { @@ -81,20 +79,22 @@ public class SecurityRequestFilter implements ContainerRequestFilter { try { - String authHeader = requestContext.getHeaderString(AUTHORIZATION_HEADER); + String authHeader = requestContext.getHeaderString("Authorization"); if (authHeader != null) { try { - User user; - if (authHeader.startsWith(BEARER_PREFIX)) { - user = loginService.login(authHeader.substring(BEARER_PREFIX.length())); + LoginResult loginResult; + if (authHeader.startsWith("Bearer ")) { + loginResult = loginService.login(authHeader.substring(7)); } else { String[] auth = decodeBasicAuth(authHeader); - user = loginService.login(auth[0], auth[1]); + loginResult = loginService.login(auth[0], auth[1], null); } - if (user != null) { + if (loginResult != null) { + User user = loginResult.getUser(); statisticsManager.registerRequest(user.getId()); - securityContext = new UserSecurityContext(new UserPrincipal(user.getId())); + securityContext = new UserSecurityContext( + new UserPrincipal(user.getId(), loginResult.getExpiration())); } } catch (StorageException | GeneralSecurityException | IOException e) { throw new WebApplicationException(e); @@ -103,14 +103,19 @@ public class SecurityRequestFilter implements ContainerRequestFilter { } else if (request.getSession() != null) { Long userId = (Long) request.getSession().getAttribute(SessionResource.USER_ID_KEY); + Date expiration = (Date) request.getSession().getAttribute(SessionResource.EXPIRATION_KEY); if (userId != null) { - statisticsManager.registerRequest(userId); - securityContext = new UserSecurityContext(new UserPrincipal(userId)); + User user = injector.getInstance(PermissionsService.class).getUser(userId); + if (user != null) { + user.checkDisabled(); + statisticsManager.registerRequest(userId); + securityContext = new UserSecurityContext(new UserPrincipal(userId, expiration)); + } } } - } catch (SecurityException e) { + } catch (SecurityException | StorageException e) { LOGGER.warn("Authentication error", e); } @@ -120,8 +125,9 @@ public class SecurityRequestFilter implements ContainerRequestFilter { Method method = resourceInfo.getResourceMethod(); if (!method.isAnnotationPresent(PermitAll.class)) { Response.ResponseBuilder responseBuilder = Response.status(Response.Status.UNAUTHORIZED); - if (!XML_HTTP_REQUEST.equals(request.getHeader(X_REQUESTED_WITH))) { - responseBuilder.header(WWW_AUTHENTICATE, BASIC_REALM); + String accept = request.getHeader("Accept"); + if (accept != null && accept.contains("text/html")) { + responseBuilder.header("WWW-Authenticate", "Basic realm=\"api\""); } throw new WebApplicationException(responseBuilder.build()); } diff --git a/src/main/java/org/traccar/api/security/UserPrincipal.java b/src/main/java/org/traccar/api/security/UserPrincipal.java index 18b84a0e1..83bd06fe9 100644 --- a/src/main/java/org/traccar/api/security/UserPrincipal.java +++ b/src/main/java/org/traccar/api/security/UserPrincipal.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 - 2020 Anton Tananaev (anton@traccar.org) + * Copyright 2015 - 2023 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,19 +16,26 @@ package org.traccar.api.security; import java.security.Principal; +import java.util.Date; public class UserPrincipal implements Principal { private final long userId; + private final Date expiration; - public UserPrincipal(long userId) { + public UserPrincipal(long userId, Date expiration) { this.userId = userId; + this.expiration = expiration; } public Long getUserId() { return userId; } + public Date getExpiration() { + return expiration; + } + @Override public String getName() { return null; diff --git a/src/main/java/org/traccar/api/security/UserSecurityContext.java b/src/main/java/org/traccar/api/security/UserSecurityContext.java index 97df6b6c7..f7adeac64 100644 --- a/src/main/java/org/traccar/api/security/UserSecurityContext.java +++ b/src/main/java/org/traccar/api/security/UserSecurityContext.java @@ -15,7 +15,7 @@ */ package org.traccar.api.security; -import javax.ws.rs.core.SecurityContext; +import jakarta.ws.rs.core.SecurityContext; import java.security.Principal; public class UserSecurityContext implements SecurityContext { diff --git a/src/main/java/org/traccar/api/signature/CryptoManager.java b/src/main/java/org/traccar/api/signature/CryptoManager.java index 249d5bd97..71f56e0fb 100644 --- a/src/main/java/org/traccar/api/signature/CryptoManager.java +++ b/src/main/java/org/traccar/api/signature/CryptoManager.java @@ -20,8 +20,8 @@ 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 jakarta.inject.Inject; +import jakarta.inject.Singleton; import java.security.GeneralSecurityException; import java.security.KeyFactory; import java.security.KeyPair; diff --git a/src/main/java/org/traccar/api/signature/TokenManager.java b/src/main/java/org/traccar/api/signature/TokenManager.java index 6a0d90b40..824433b08 100644 --- a/src/main/java/org/traccar/api/signature/TokenManager.java +++ b/src/main/java/org/traccar/api/signature/TokenManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 Anton Tananaev (anton@traccar.org) + * Copyright 2022 - 2023 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. @@ -20,8 +20,8 @@ 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 jakarta.inject.Inject; +import jakarta.inject.Singleton; import java.io.IOException; import java.security.GeneralSecurityException; import java.util.Date; @@ -35,11 +35,19 @@ public class TokenManager { private final ObjectMapper objectMapper; private final CryptoManager cryptoManager; - public static class Data { + public static class TokenData { @JsonProperty("u") private long userId; @JsonProperty("e") private Date expiration; + + public long getUserId() { + return userId; + } + + public Date getExpiration() { + return expiration; + } } @Inject @@ -54,7 +62,7 @@ public class TokenManager { public String generateToken( long userId, Date expiration) throws IOException, GeneralSecurityException, StorageException { - Data data = new Data(); + TokenData data = new TokenData(); data.userId = userId; if (expiration != null) { data.expiration = expiration; @@ -65,13 +73,13 @@ public class TokenManager { return Base64.encodeBase64URLSafeString(cryptoManager.sign(encoded)); } - public long verifyToken(String token) throws IOException, GeneralSecurityException, StorageException { + public TokenData verifyToken(String token) throws IOException, GeneralSecurityException, StorageException { byte[] encoded = cryptoManager.verify(Base64.decodeBase64(token)); - Data data = objectMapper.readValue(encoded, Data.class); + TokenData data = objectMapper.readValue(encoded, TokenData.class); if (data.expiration.before(new Date())) { throw new SecurityException("Token has expired"); } - return data.userId; + return data; } } |