Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@ Business metrics:
| link_visit_total | Visit to the all shortlinks | result_status (status of visits) |
| handled_error_total | REST API errors | code (code in result block) |
| qr_generated | Count of generated QR codes | - |
// TODO Unacth request


Security
-------


Configuration
Expand Down Expand Up @@ -152,6 +157,7 @@ Configuration
| Custom.QR | QR_HEIGHT | QR image height | positive int | 200 |
| Custom.QR | QR_WIDTH | QR image width | positive int | 200 |
| Custom.QR | QR_ENDPOINT | Full endpoint of shortlink, must have %s in place when shortlink will be placed | string | http://localhost:80/s/%s |
// TODO Security params



Expand Down Expand Up @@ -206,3 +212,10 @@ app initializing and starts API interaction of some use-case of app: create the

The main reason of it to awoid to cover the all app of units and test the main processed of usage in almost real enviroment:
DB in container, REST API interaction step-by-step


### BaseProtoFramework

There is a simple base framework for build Service and Model,
see `BaseService`, `BaseCRUDService` for details for service and `Model` and `Activating` for entity.
Provides some basic CRUD operation for Entity
14 changes: 9 additions & 5 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,27 +55,31 @@ Phase 4 - Features part 1

* ~~QR code generator for links~~

* ~~Separate Swagger group API for private and public~~


Phase 5 - Features part 2
-------------------------

* Security
- Spring security with authorized API requests
- Add test for all above

* ~~Separate Swagger group API for private and public~~
* Password auth for links visit (custom popup or browser default pop)


Phase 5 - Features part 2
Phase 6 - Features part 3
-------------------------

* Custom links short URL
- Custom generator with checking with existing
- Generator resolver
- Stop list of some links

* Password auth for links visit (custom popup or browser default pop)

* Saves not found links in visits table


Phase 6 - Features part 3
Phase 7 - Features part 4
-------------------------

* SonarKube check outside of IDEA (SonarLint) in build CI with quality gate
Expand Down
11 changes: 11 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,22 @@
<artifactId>flyway-core</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>${spring-docs.version}</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-security</artifactId>
<version>${spring-docs.version}</version>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
Expand Down
15 changes: 15 additions & 0 deletions src/main/java/dev/alnat/tinylinkshortener/config/Constants.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package dev.alnat.tinylinkshortener.config;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;

/**
* Created by @author AlNat on 27.01.2023.
* Licensed by Apache License, Version 2.0
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class Constants {

public static final String AUTH_HEADER_NAME = "X-USER-KEY";

}
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package dev.alnat.tinylinkshortener.config;

import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.media.DateTimeSchema;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.media.StringSchema;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.springdoc.core.GroupedOpenApi;
import org.springdoc.core.customizers.OpenApiCustomiser;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

Expand All @@ -17,6 +21,7 @@
* Licensed by Apache License, Version 2.0
*/
@Configuration
@ConditionalOnProperty(name = "springdoc.api-docs.enabled", havingValue = "true")
public class OpenAPIConfiguration {

@Bean
Expand Down Expand Up @@ -65,7 +70,18 @@ public GroupedOpenApi privateOpenApi() {
.group("private")
.displayName("private_api")
.pathsToMatch("/api/**")
.addOpenApiCustomiser(securedApiCustomizer())
.build();
}

public OpenApiCustomiser securedApiCustomizer() {
return openApi -> openApi
.addSecurityItem(new SecurityRequirement().addList("apiKey"))
.components(new Components()
.addSecuritySchemes("apiKey", new SecurityScheme()
.type(SecurityScheme.Type.APIKEY)
.in(SecurityScheme.In.HEADER)
.name(Constants.AUTH_HEADER_NAME)));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package dev.alnat.tinylinkshortener.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import dev.alnat.tinylinkshortener.mapper.UserMapper;
import dev.alnat.tinylinkshortener.security.HeaderKeySecurityFilter;
import dev.alnat.tinylinkshortener.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.savedrequest.NullRequestCache;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
@RequiredArgsConstructor
public class SecurityConfig {

private static final RequestMatcher NOT_AUTH_ENDPOINTS = new OrRequestMatcher(
new AntPathRequestMatcher("/s/**"), // redirects
new AntPathRequestMatcher("/**/swagger-resources/**"),
new AntPathRequestMatcher("/**/swagger-ui.html/**"),
new AntPathRequestMatcher("/**/swagger-ui/**"),
new AntPathRequestMatcher("/**/swagger-ui.html"),
new AntPathRequestMatcher("/favicon.ico"),
new AntPathRequestMatcher("/webjars/**"),
new AntPathRequestMatcher("/v3/api-docs/**"),
new AntPathRequestMatcher("/v3/api-docs"),
new AntPathRequestMatcher("/actuator/**")
);

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Bean
public HeaderKeySecurityFilter securityFilter(final UserService service,
final PasswordEncoder encoder,
final ObjectMapper mapper,
final UserMapper userMapper) {
return new HeaderKeySecurityFilter(service, NOT_AUTH_ENDPOINTS, encoder, mapper, userMapper);
}

@Bean
public SecurityFilterChain filterChain(HttpSecurity http, HeaderKeySecurityFilter filter) throws Exception {
return http
.addFilterBefore(filter, BasicAuthenticationFilter.class)
.requestCache().requestCache(new NullRequestCache()) // Disable caching
.and()
.csrf().disable()
.authorizeRequests().requestMatchers(NOT_AUTH_ENDPOINTS).permitAll()
.anyRequest().authenticated()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,11 @@ public class LinkController {
public Result<LinkOutDTO> create(
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "DTO for new short link request", required = true)
@RequestBody @Valid final LinkInDTO dto) {
// TODO CheckAnonymous
return linkService.create(dto);
}

// TODO Secured User, Admin
@Operation(summary = "Search shortlink by ID")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Request completed, see result in code field in response")
Expand All @@ -55,6 +57,7 @@ public Result<LinkOutDTO> find(@Parameter(in = ParameterIn.PATH, description = "
return linkService.find(id);
}

// TODO Secured User, Admin
@Operation(summary = "Search shortlink by shortlink (as lookup method)")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Request completed, see result in code field in response")
Expand All @@ -65,6 +68,7 @@ public Result<LinkOutDTO> find(@Parameter(in = ParameterIn.QUERY, description =
return linkService.find(shortLink);
}

// TODO Secured User, Admin
@Operation(summary = "Deactivate shortlink by ID")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Request completed, see result in code field in response")
Expand All @@ -75,6 +79,7 @@ public Result<Void> deactivate(@Parameter(in = ParameterIn.PATH, description = "
return linkService.deactivate(id);
}

// TODO Secured User, Admin
@Operation(summary = "Deactivate shortlink by shortlink")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Request completed, see result in code field in response")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.util.StreamUtils;
import org.springframework.web.bind.annotation.*;

Expand All @@ -32,7 +33,7 @@
* Licensed by Apache License, Version 2.0
*/
@Slf4j
@RestController
@Controller
@Setter
@RequestMapping(value = "/s/", produces = MediaType.APPLICATION_JSON_VALUE)
@Tag(name = "Controller for requesting shortlinks", description = "Front-end controller for users")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package dev.alnat.tinylinkshortener.controller;

/**
* Created by @author AlNat on 28.01.2023.
* Licensed by Apache License, Version 2.0
*/
public class UserController {

// TODO CRUD (only by admin)

}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public class VisitController {
@Value("${custom.paging.default-timeout}")
private Duration pagingTimeout;

// TODO RoleUser, RoleAdmin
@Operation(summary = "Paging visit results for short link")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Request completed, see result in code field in response")
Expand All @@ -75,6 +76,7 @@ public DeferredResult<LinkVisitPageResult> searchRawStatistics(
return deferredResult;
}

// TODO RoleUser
@Operation(summary = "Aggregating visit results for short link")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Request completed, see result in code field in response")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.lang.Nullable;
import org.springframework.security.core.AuthenticationException;
import org.springframework.validation.FieldError;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
Expand Down Expand Up @@ -122,6 +123,15 @@ protected ResponseEntity<Object> handleExceptionInternal(Exception ex,
.body(ResultFactory.error("Internal Server Error, traceId=[" + traceId + "]"));
}

@ExceptionHandler(AuthenticationException.class)
public ResponseEntity<Object> handleAuthenticationException(AuthenticationException ex) {
log.warn("Security exception", ex);
return new ResponseEntity<>(
ResultFactory.unauthorized(),
HttpStatus.OK
);
}

/**
* Global exception
*/
Expand Down
17 changes: 17 additions & 0 deletions src/main/java/dev/alnat/tinylinkshortener/dto/UserInDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package dev.alnat.tinylinkshortener.dto;

import dev.alnat.tinylinkshortener.model.enums.UserRole;

/**
* Created by @author AlNat on 28.01.2023.
* Licensed by Apache License, Version 2.0
*/
public class UserInDTO {

private String name;

private UserRole role;

private String key;

}
33 changes: 33 additions & 0 deletions src/main/java/dev/alnat/tinylinkshortener/dto/UserOutDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package dev.alnat.tinylinkshortener.dto;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import dev.alnat.tinylinkshortener.model.enums.UserRole;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

/**
* Created by @author AlNat on 28.01.2023.
* Licensed by Apache License, Version 2.0
*/
@Data
@ToString
@AllArgsConstructor
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
@Schema(description = "Information about the user")
public class UserOutDTO {

// TODO Swagger

private Integer id;

private String name;

private UserRole role;

}
Loading