diff --git a/build.gradle.kts b/build.gradle.kts index 25dcb10..0a5fc5a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -40,6 +40,7 @@ dependencies { implementation(libs.jetbrains.annotations) implementation(libs.springdoc.openapi.starter.webmvc.ui) implementation("org.springframework.boot", "spring-boot-starter-data-mongodb") + implementation("org.springframework.boot", "spring-boot-starter-security") implementation("org.springframework.boot", "spring-boot-starter-validation") implementation("org.springframework.boot", "spring-boot-starter-web") testImplementation("org.springframework.boot", "spring-boot-starter-test") { diff --git a/src/main/java/io/papermc/bibliothek/configuration/SecurityConfiguration.java b/src/main/java/io/papermc/bibliothek/configuration/SecurityConfiguration.java new file mode 100644 index 0000000..0cce2b9 --- /dev/null +++ b/src/main/java/io/papermc/bibliothek/configuration/SecurityConfiguration.java @@ -0,0 +1,58 @@ +/* + * This file is part of bibliothek, licensed under the MIT License. + * + * Copyright (c) 2019-2024 PaperMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package io.papermc.bibliothek.configuration; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@EnableWebSecurity +public class SecurityConfiguration { + @Bean + SecurityFilterChain securityFilterChain(final HttpSecurity http) throws Exception { + http + .authorizeHttpRequests(requests -> { + requests + .requestMatchers(HttpMethod.POST, "/v2/projects/*/versions/*/builds") + .hasAnyRole("CREATE_BUILD") + .anyRequest() + .permitAll(); + }) + .csrf(AbstractHttpConfigurer::disable) + .httpBasic(configurer -> configurer.realmName("bibliothek")); + return http.build(); + } + + @Bean + PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/src/main/java/io/papermc/bibliothek/controller/v2/PublishController.java b/src/main/java/io/papermc/bibliothek/controller/v2/PublishController.java new file mode 100644 index 0000000..e3b39ac --- /dev/null +++ b/src/main/java/io/papermc/bibliothek/controller/v2/PublishController.java @@ -0,0 +1,124 @@ +/* + * This file is part of bibliothek, licensed under the MIT License. + * + * Copyright (c) 2019-2024 PaperMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package io.papermc.bibliothek.controller.v2; + +import io.papermc.bibliothek.database.model.Build; +import io.papermc.bibliothek.database.model.Project; +import io.papermc.bibliothek.database.model.Version; +import io.papermc.bibliothek.database.model.VersionFamily; +import io.papermc.bibliothek.database.repository.BuildCollection; +import io.papermc.bibliothek.database.repository.ProjectCollection; +import io.papermc.bibliothek.database.repository.VersionCollection; +import io.papermc.bibliothek.database.repository.VersionFamilyCollection; +import io.papermc.bibliothek.exception.ProjectNotFound; +import io.swagger.v3.oas.annotations.Hidden; +import java.net.URI; +import java.time.Instant; +import java.util.List; +import java.util.Map; +import org.jetbrains.annotations.Nullable; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; + +@RestController +@RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE) +@SuppressWarnings("checkstyle:FinalClass") +public class PublishController { + private final ProjectCollection projects; + private final VersionFamilyCollection families; + private final VersionCollection versions; + private final BuildCollection builds; + + @Autowired + private PublishController( + final ProjectCollection projects, + final VersionFamilyCollection families, + final VersionCollection versions, + final BuildCollection builds + ) { + this.projects = projects; + this.families = families; + this.versions = versions; + this.builds = builds; + } + + @Hidden + @PostMapping( + path = "/v2/projects/{project:[a-z]+}/versions/{version:" + Version.PATTERN + "}/builds", + consumes = MediaType.APPLICATION_JSON_VALUE, + produces = MediaType.APPLICATION_JSON_VALUE + ) + public ResponseEntity createBuild(@RequestBody final PublishRequest request) { + final Project project = this.projects.findByName(request.project()).orElseThrow(ProjectNotFound::new); + final VersionFamily family = this.families.findByProjectAndName(project._id(), request.family()) + .orElseGet(() -> this.families.save(new VersionFamily(null, project._id(), request.family(), request.familyTime()))); + final Version version = this.versions.findByProjectAndName(project._id(), request.version()) + .orElseGet(() -> this.versions.save(new Version(null, project._id(), family._id(), request.version(), request.versionTime()))); + final Build build = this.builds.insert(new Build( + null, + project._id(), + version._id(), + request.build(), + request.time(), + request.changes(), + request.downloads(), + request.channel(), + false + )); + return ResponseEntity.created(URI.create( + MvcUriComponentsBuilder + .fromMappingName("project.version.build") + .arg(0, project.name()) + .arg(1, version.name()) + .arg(2, build.number()) + .build() + )).body(new PublishResponse(true, build)); + } + + private record PublishRequest( + String project, + String family, + @Nullable Instant familyTime, + String version, + @Nullable Instant versionTime, + int build, + Instant time, + Build.Channel channel, + List changes, + Map downloads + ) { + } + + private record PublishResponse( + boolean success, + Build build + ) { + } +} diff --git a/src/main/java/io/papermc/bibliothek/controller/v2/VersionBuildController.java b/src/main/java/io/papermc/bibliothek/controller/v2/VersionBuildController.java index b4b4d47..514a8ae 100644 --- a/src/main/java/io/papermc/bibliothek/controller/v2/VersionBuildController.java +++ b/src/main/java/io/papermc/bibliothek/controller/v2/VersionBuildController.java @@ -78,7 +78,10 @@ private VersionBuildController( ), responseCode = "200" ) - @GetMapping("/v2/projects/{project:[a-z]+}/versions/{version:" + Version.PATTERN + "}/builds/{build:\\d+}") + @GetMapping( + name = "project.version.build", + path = "/v2/projects/{project:[a-z]+}/versions/{version:" + Version.PATTERN + "}/builds/{build:\\d+}" + ) @Operation(summary = "Gets information related to a specific build.") public ResponseEntity build( @Parameter(name = "project", description = "The project identifier.", example = "paper") diff --git a/src/main/java/io/papermc/bibliothek/database/model/Build.java b/src/main/java/io/papermc/bibliothek/database/model/Build.java index efaaacf..4b06811 100644 --- a/src/main/java/io/papermc/bibliothek/database/model/Build.java +++ b/src/main/java/io/papermc/bibliothek/database/model/Build.java @@ -23,6 +23,7 @@ */ package io.papermc.bibliothek.database.model; +import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; @@ -54,16 +55,19 @@ public record Build( @Nullable Boolean promoted ) { public Channel channelOrDefault() { - return Objects.requireNonNullElse(this.channel(), Build.Channel.DEFAULT); + return Objects.requireNonNullElse(this.channel(), Channel.DEFAULT); } public boolean promotedOrDefault() { return Objects.requireNonNullElse(this.promoted(), false); } + // These annotations are in reverse, but we can't fix this in v2. public enum Channel { + @JsonAlias("DEFAULT") @JsonProperty("default") DEFAULT, + @JsonAlias("EXPERIMENTAL") @JsonProperty("experimental") EXPERIMENTAL; } diff --git a/src/main/java/io/papermc/bibliothek/database/model/User.java b/src/main/java/io/papermc/bibliothek/database/model/User.java new file mode 100644 index 0000000..f53db3f --- /dev/null +++ b/src/main/java/io/papermc/bibliothek/database/model/User.java @@ -0,0 +1,38 @@ +/* + * This file is part of bibliothek, licensed under the MIT License. + * + * Copyright (c) 2019-2024 PaperMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package io.papermc.bibliothek.database.model; + +import java.util.Set; +import org.bson.types.ObjectId; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +@Document(collection = "users") +public record User( + @Id ObjectId _id, + String username, + String password, + Set authorities +) { +} diff --git a/src/main/java/io/papermc/bibliothek/database/repository/UserRepository.java b/src/main/java/io/papermc/bibliothek/database/repository/UserRepository.java new file mode 100644 index 0000000..8769a71 --- /dev/null +++ b/src/main/java/io/papermc/bibliothek/database/repository/UserRepository.java @@ -0,0 +1,34 @@ +/* + * This file is part of bibliothek, licensed under the MIT License. + * + * Copyright (c) 2019-2024 PaperMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package io.papermc.bibliothek.database.repository; + +import io.papermc.bibliothek.database.model.User; +import org.bson.types.ObjectId; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface UserRepository extends MongoRepository { + User findByUsername(final String username); +} diff --git a/src/main/java/io/papermc/bibliothek/filter/CorsFilter.java b/src/main/java/io/papermc/bibliothek/filter/CorsFilter.java index 179caa9..d40dc75 100644 --- a/src/main/java/io/papermc/bibliothek/filter/CorsFilter.java +++ b/src/main/java/io/papermc/bibliothek/filter/CorsFilter.java @@ -34,12 +34,11 @@ @WebFilter("/*") public class CorsFilter implements Filter { - @Override public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) throws IOException, ServletException { final HttpServletResponse httpServletResponse = (HttpServletResponse) response; - httpServletResponse.setHeader("Access-Control-Allow-Origin", "*"); httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET"); + httpServletResponse.setHeader("Access-Control-Allow-Origin", "*"); chain.doFilter(request, response); } } diff --git a/src/main/java/io/papermc/bibliothek/service/UserDetailsServiceImpl.java b/src/main/java/io/papermc/bibliothek/service/UserDetailsServiceImpl.java new file mode 100644 index 0000000..9aa17da --- /dev/null +++ b/src/main/java/io/papermc/bibliothek/service/UserDetailsServiceImpl.java @@ -0,0 +1,60 @@ +/* + * This file is part of bibliothek, licensed under the MIT License. + * + * Copyright (c) 2019-2024 PaperMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package io.papermc.bibliothek.service; + +import io.papermc.bibliothek.database.model.User; +import io.papermc.bibliothek.database.repository.UserRepository; +import java.util.stream.Collectors; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +@Service +public class UserDetailsServiceImpl implements UserDetailsService { + private final UserRepository users; + + @Autowired + public UserDetailsServiceImpl(final UserRepository users) { + this.users = users; + } + + @Override + public UserDetails loadUserByUsername(final String username) throws UsernameNotFoundException { + final User user = this.users.findByUsername(username); + if (user == null) { + throw new UsernameNotFoundException(username); + } + return new org.springframework.security.core.userdetails.User( + user.username(), + user.password(), + user.authorities() + .stream() + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toSet()) + ); + } +}