Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master' into 17
Browse files Browse the repository at this point in the history
  • Loading branch information
dukris committed Jan 25, 2024
2 parents d56eb8a + 5e15108 commit 9eed9ee
Show file tree
Hide file tree
Showing 12 changed files with 493 additions and 20 deletions.
51 changes: 51 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
[![EO principles respected here](https://www.elegantobjects.org/badge.svg)](https://www.elegantobjects.org)
[![DevOps By Rultor.com](https://www.rultor.com/b/ac-californium/api)](https://www.rultor.com/p/ac-californium/api)
[![We recommend IntelliJ IDEA](https://www.elegantobjects.org/intellij-idea.svg)](https://www.jetbrains.com/idea/)

[![mvn](https://github.com/tracehubpm/pmo/actions/workflows/mvn.yml/badge.svg)](https://github.com/tracehubpm/pmo/actions/workflows/mvn.yml)
[![codecov](https://codecov.io/gh/tracehubpm/pmo/graph/badge.svg?token=rnRZ3e6s6e)](https://codecov.io/gh/tracehubpm/pmo)
[![PDD status](http://www.0pdd.com/svg?name=tracehubpm/pmo)](http://www.0pdd.com/p?name=tracehubpm/pmo)

Project architect: [@hizmailovich](https://github.com/hizmailovich)

Project registry, facilities and its governance.

### How to use?

Project Management Office (PMO) is a RESTful JSON API with ability to
manipulate with projects. To check this RESTful API, all you need is Swagger Docs,
it can be found here: `/swagger-ui/index.html`.

**Functionality:**

* It allows to log in using login and password.
* It allows to log in using such social networks as Google and GitHub.
* It allows to create a project.

After project creation bot [@tracehubgit](https://github.com/tracehubgit) will be invited
to the repository and a `new` label for issues will be added. Moreover, a webhook for `push` events will be
created to notify PMO about changes in the repository.

### How to run?

Before you start the app locally, you need to run Keycloak and PostgreSQL using such command:

```bash
$ docker-compose up -d
```

### How to contribute?

Fork repository, make changes, send us a [pull request](https://www.yegor256.com/2014/04/15/github-guidelines.html).
We will review your changes and apply them to the `master` branch shortly,
provided they don't violate our quality standards. To avoid frustration,
before sending us your pull request please run full Maven build:

```bash
$ mvn clean install
```

You will need Maven 3.8.7+ and Java 17+.

All the things above will be run by [Rultor.com](http://rultor.com/)
and CI [gate](https://github.com/tracehub/pmo/actions).
19 changes: 4 additions & 15 deletions src/main/java/git/tracehub/pmo/controller/ProjectController.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.web.bind.annotation.GetMapping;
Expand Down Expand Up @@ -79,13 +80,7 @@ public List<Project> byUser(@AuthenticationPrincipal final Jwt jwt) {
* @return Project
*/
@GetMapping("/{id}")
/*
* @todo #1:45min/DEV check if authenticated user can access the Project.
* we need create security checks that will made a statement
* does authenticated user can access the project or not:
* if project is public, every one can see it;
* otherwise user must be individual performer or team member.
*/
@PreAuthorize("@hasProject.validate(#id)")
public Project byId(@PathVariable final UUID id) {
return this.projects.byId(id);
}
Expand All @@ -98,14 +93,8 @@ public Project byId(@PathVariable final UUID id) {
* @return Project
* @checkstyle MethodBodyCommentsCheck (20 lines)
*/
@PostMapping
/*
* @todo #1:45min/DEV check if authenticated user can create a new
* project. We need to create security checks that will make a statement
* does authenticated user can create a new project or not:
* if project is public, every one can create it;
* otherwise we need to request a payment from the user.
*/
@PostMapping()
@PreAuthorize("hasAuthority('user_github')")
@ResponseStatus(HttpStatus.CREATED)
public Project employ(
@RequestBody final Project project,
Expand Down
50 changes: 50 additions & 0 deletions src/main/java/git/tracehub/pmo/security/AuthoritiesConverter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright (c) 2023-2024 Tracehub.git
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to read
* the Software only. Permissions is hereby NOT GRANTED to use, copy, modify,
* merge, publish, distribute, sublicense, and/or sell copies 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 NON-INFRINGEMENT. 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 git.tracehub.pmo.security;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;

/**
* Converter for granted authorities.
*
* @since 0.0.0
*/
public final class AuthoritiesConverter implements Converter<Jwt, JwtAuthenticationToken> {

@Override
public JwtAuthenticationToken convert(final Jwt jwt) {
final Map<String, Object> map = jwt.getClaimAsMap("realm_access");
final List<SimpleGrantedAuthority> authorities = new ArrayList<>(5);
if (map != null && !map.isEmpty()) {
authorities.addAll(
((List<String>) map.get("roles"))
.stream()
.map(SimpleGrantedAuthority::new)
.toList()
);
}
return new JwtAuthenticationToken(jwt, authorities);
}

}
7 changes: 3 additions & 4 deletions src/main/java/git/tracehub/pmo/security/ExistsRole.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,9 @@

package git.tracehub.pmo.security;

import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.cactoos.Scalar;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;

/**
Expand All @@ -43,8 +42,8 @@ public final class ExistsRole implements Scalar<Boolean> {

@Override
public Boolean value() {
final Map<String, Object> map = this.jwt.getClaimAsMap("realm_access");
return ((List<String>) map.get("roles")).contains(this.role);
return new AuthoritiesConverter().convert(this.jwt).getAuthorities()
.contains(new SimpleGrantedAuthority(this.role));
}

}
4 changes: 3 additions & 1 deletion src/main/java/git/tracehub/pmo/security/WebConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,9 @@ public SecurityFilterChain client(final HttpSecurity http) {
}
)
).oauth2ResourceServer(
configurer -> configurer.jwt(Customizer.withDefaults())
configurer -> configurer.jwt(
jwt -> jwt.jwtAuthenticationConverter(new AuthoritiesConverter())
)
).build();
}

Expand Down
36 changes: 36 additions & 0 deletions src/main/java/git/tracehub/pmo/security/expression/Expression.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright (c) 2023-2024 Tracehub.git
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to read
* the Software only. Permissions is hereby NOT GRANTED to use, copy, modify,
* merge, publish, distribute, sublicense, and/or sell copies 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 NON-INFRINGEMENT. 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 git.tracehub.pmo.security.expression;

/**
* Security expression.
*
* @param <T> Type of argument
* @since 0.0.0
*/
public interface Expression<T> {

/**
* Validate.
*
* @param arg Argument
* @return Result
*/
boolean validate(T arg);

}
61 changes: 61 additions & 0 deletions src/main/java/git/tracehub/pmo/security/expression/HasProject.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright (c) 2023-2024 Tracehub.git
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to read
* the Software only. Permissions is hereby NOT GRANTED to use, copy, modify,
* merge, publish, distribute, sublicense, and/or sell copies 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 NON-INFRINGEMENT. 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 git.tracehub.pmo.security.expression;

import git.tracehub.pmo.project.Project;
import git.tracehub.pmo.project.Projects;
import git.tracehub.pmo.security.ClaimOf;
import java.util.UUID;
import java.util.stream.Stream;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.stereotype.Component;

/**
* Has project.
*
* @checkstyle DesignForExtensionCheck (30 lines)
* @since 0.0.0
*/
@Component
@RequiredArgsConstructor
public class HasProject implements Expression<UUID> {

/**
* Projects.
*/
private final Projects projects;

@Override
public boolean validate(final UUID project) {
final Jwt jwt = (Jwt) SecurityContextHolder.getContext()
.getAuthentication()
.getPrincipal();
return Stream.concat(
this.projects.byUser(
new ClaimOf(jwt, "email").value()
).stream(),
this.projects.byUser(
new ClaimOf(jwt, "preferred_username").value()
).stream()
).map(Project::getId)
.anyMatch(id -> id.equals(project));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright (c) 2023-2024 Tracehub.git
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to read
* the Software only. Permissions is hereby NOT GRANTED to use, copy, modify,
* merge, publish, distribute, sublicense, and/or sell copies 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 NON-INFRINGEMENT. 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.
*/

/**
* Security expressions.
*
* @since 0.0.0
*/
package git.tracehub.pmo.security.expression;
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* Copyright (c) 2023-2024 Tracehub.git
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to read
* the Software only. Permissions is hereby NOT GRANTED to use, copy, modify,
* merge, publish, distribute, sublicense, and/or sell copies 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 NON-INFRINGEMENT. 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 git.tracehub.pmo.security;

import org.cactoos.list.ListOf;
import org.cactoos.map.MapEntry;
import org.cactoos.map.MapOf;
import org.hamcrest.MatcherAssert;
import org.hamcrest.core.IsEqual;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;

/**
* Test suite for {@link AuthoritiesConverter}.
*
* @since 0.0.0
*/
@ExtendWith(MockitoExtension.class)
final class AuthoritiesConverterTest {

/**
* JWT.
*/
@Mock
private Jwt jwt;

@Test
void convertsJwtSuccessfully() {
Mockito.when(this.jwt.getClaimAsMap("realm_access")).thenReturn(
new MapOf<>(
new MapEntry<>("roles", new ListOf<>("role"))
)
);
final JwtAuthenticationToken token = new AuthoritiesConverter().convert(this.jwt);
MatcherAssert.assertThat(
"List of authorities is empty",
token.getAuthorities().isEmpty(),
new IsEqual<>(false)
);
MatcherAssert.assertThat(
"List of authorities %s doesn't include role "
.formatted(token.getAuthorities()),
token.getAuthorities().contains(
new SimpleGrantedAuthority("role")
),
new IsEqual<>(true)
);
}

@Test
void returnsEmptyListWhenMapIsEmpty() {
final JwtAuthenticationToken token = new AuthoritiesConverter().convert(this.jwt);
MatcherAssert.assertThat(
"List of authorities %s isn't empty"
.formatted(token.getAuthorities()),
token.getAuthorities().isEmpty(),
new IsEqual<>(true)
);
}

@Test
void returnsEmptyListWhenMapIsNull() {
Mockito.when(this.jwt.getClaimAsMap("realm_access")).thenReturn(null);
final JwtAuthenticationToken token = new AuthoritiesConverter().convert(this.jwt);
MatcherAssert.assertThat(
"List of authorities %s isn't empty"
.formatted(token.getAuthorities()),
token.getAuthorities().isEmpty(),
new IsEqual<>(true)
);
}

}
Loading

0 comments on commit 9eed9ee

Please sign in to comment.