diff --git a/scm-it/src/test/java/sonia/scm/it/MeITCase.java b/scm-it/src/test/java/sonia/scm/it/MeITCase.java index ce6593ef11..89c6eeb7b8 100644 --- a/scm-it/src/test/java/sonia/scm/it/MeITCase.java +++ b/scm-it/src/test/java/sonia/scm/it/MeITCase.java @@ -1,13 +1,10 @@ package sonia.scm.it; -import org.junit.Assert; import org.junit.Before; import org.junit.Test; import sonia.scm.it.utils.ScmRequests; import sonia.scm.it.utils.TestData; -import static org.assertj.core.api.Assertions.assertThat; - public class MeITCase { @Before @@ -23,9 +20,6 @@ public class MeITCase { .requestIndexResource(TestData.USER_SCM_ADMIN, TestData.USER_SCM_ADMIN) .requestMe() .assertStatusCode(200) - .assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE)) - .assertPassword(Assert::assertNull) - .assertType(s -> assertThat(s).isEqualTo("xml")) .requestChangePassword(TestData.USER_SCM_ADMIN, newPassword) .assertStatusCode(204); // assert password is changed -> login with the new Password than undo changes @@ -33,7 +27,6 @@ public class MeITCase { .requestIndexResource(TestData.USER_SCM_ADMIN, newPassword) .requestMe() .assertStatusCode(200) - .assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE))// still admin .requestChangePassword(newPassword, TestData.USER_SCM_ADMIN) .assertStatusCode(204); } @@ -49,9 +42,6 @@ public class MeITCase { .requestIndexResource(username, password) .requestMe() .assertStatusCode(200) - .assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.FALSE)) - .assertPassword(Assert::assertNull) - .assertType(s -> assertThat(s).isEqualTo("xml")) .requestChangePassword(password, newPassword) .assertStatusCode(204); // assert password is changed -> login with the new Password than undo changes @@ -72,9 +62,6 @@ public class MeITCase { .requestIndexResource(newUser, password) .requestMe() .assertStatusCode(200) - .assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE)) - .assertPassword(Assert::assertNull) - .assertType(s -> assertThat(s).isEqualTo(type)) .assertPasswordLinkDoesNotExists(); } } diff --git a/scm-it/src/test/java/sonia/scm/it/utils/ScmRequests.java b/scm-it/src/test/java/sonia/scm/it/utils/ScmRequests.java index bde3892773..9386f1d9c5 100644 --- a/scm-it/src/test/java/sonia/scm/it/utils/ScmRequests.java +++ b/scm-it/src/test/java/sonia/scm/it/utils/ScmRequests.java @@ -48,7 +48,7 @@ public class ScmRequests { return new IndexResponse(applyGETRequest(RestUtil.REST_BASE_URL.toString())); } - public , T extends ModelResponse> UserResponse requestUser(String username, String password, String pathParam) { + public UserResponse requestUser(String username, String password, String pathParam) { setUsername(username); setPassword(password); return new UserResponse<>(applyGETRequest(RestUtil.REST_BASE_URL.resolve("users/"+pathParam).toString()), null); @@ -195,7 +195,7 @@ public class ScmRequests { return new MeResponse<>(applyGETRequestFromLink(response, LINK_ME), this); } - public UserResponse requestUser(String username) { + public UserResponse requestUser(String username) { return new UserResponse<>(applyGETRequestFromLinkWithParams(response, LINK_USERS, username), this); } @@ -307,19 +307,24 @@ public class ScmRequests { } - public class MeResponse extends UserResponse, PREV> { + public class MeResponse extends ModelResponse, PREV> { + public static final String LINKS_PASSWORD_HREF = "_links.password.href"; public MeResponse(Response response, PREV previousResponse) { super(response, previousResponse); } - public ChangePasswordResponse requestChangePassword(String oldPassword, String newPassword) { + public MeResponse assertPasswordLinkDoesNotExists() { + return assertPropertyPathDoesNotExists(LINKS_PASSWORD_HREF); + } + + public ChangePasswordResponse requestChangePassword(String oldPassword, String newPassword) { return new ChangePasswordResponse<>(applyPUTRequestFromLink(super.response, LINKS_PASSWORD_HREF, VndMediaType.PASSWORD_CHANGE, createPasswordChangeJson(oldPassword, newPassword)), this); } } - public class UserResponse, PREV extends ModelResponse> extends ModelResponse { + public class UserResponse extends ModelResponse, PREV> { public static final String LINKS_PASSWORD_HREF = "_links.password.href"; @@ -327,23 +332,23 @@ public class ScmRequests { super(response, previousResponse); } - public SELF assertPassword(Consumer assertPassword) { + public UserResponse assertPassword(Consumer assertPassword) { return super.assertSingleProperty(assertPassword, "password"); } - public SELF assertType(Consumer assertType) { + public UserResponse assertType(Consumer assertType) { return assertSingleProperty(assertType, "type"); } - public SELF assertAdmin(Consumer assertAdmin) { + public UserResponse assertAdmin(Consumer assertAdmin) { return assertSingleProperty(assertAdmin, "admin"); } - public SELF assertPasswordLinkDoesNotExists() { + public UserResponse assertPasswordLinkDoesNotExists() { return assertPropertyPathDoesNotExists(LINKS_PASSWORD_HREF); } - public SELF assertPasswordLinkExists() { + public UserResponse assertPasswordLinkExists() { return assertPropertyPathExists(LINKS_PASSWORD_HREF); } diff --git a/scm-ui-components/packages/ui-types/src/Me.js b/scm-ui-components/packages/ui-types/src/Me.js index 12516ade1b..f6cb9c2036 100644 --- a/scm-ui-components/packages/ui-types/src/Me.js +++ b/scm-ui-components/packages/ui-types/src/Me.js @@ -6,5 +6,6 @@ export type Me = { name: string, displayName: string, mail: string, + groups: [], _links: Links }; diff --git a/scm-ui/public/locales/en/commons.json b/scm-ui/public/locales/en/commons.json index 3196f3a328..e3e1dbf032 100644 --- a/scm-ui/public/locales/en/commons.json +++ b/scm-ui/public/locales/en/commons.json @@ -48,6 +48,7 @@ "username": "Username", "displayName": "Display Name", "mail": "E-Mail", + "groups": "Groups", "information": "Information", "change-password": "Change password", "error-title": "Error", diff --git a/scm-ui/src/containers/ProfileInfo.js b/scm-ui/src/containers/ProfileInfo.js index 9c4a5a9323..4c333174d1 100644 --- a/scm-ui/src/containers/ProfileInfo.js +++ b/scm-ui/src/containers/ProfileInfo.js @@ -1,53 +1,63 @@ -// @flow -import React from "react"; -import type { Me } from "@scm-manager/ui-types"; -import { MailLink, AvatarWrapper, AvatarImage } from "@scm-manager/ui-components"; -import { compose } from "redux"; -import { translate } from "react-i18next"; - -type Props = { - me: Me, - - // Context props - t: string => string -}; -type State = {}; - -class ProfileInfo extends React.Component { - render() { - const { me, t } = this.props; - return ( -
- -
-

- -

-
-
-
- - - - - - - - - - - - - - - -
{t("profile.username")}{me.name}
{t("profile.displayName")}{me.displayName}
{t("profile.mail")} - -
-
-
- ); - } -} - -export default compose(translate("commons"))(ProfileInfo); +// @flow +import React from "react"; +import type { Me } from "@scm-manager/ui-types"; +import { MailLink, AvatarWrapper, AvatarImage } from "@scm-manager/ui-components"; +import { compose } from "redux"; +import { translate } from "react-i18next"; + +type Props = { + me: Me, + + // Context props + t: string => string +}; +type State = {}; + +class ProfileInfo extends React.Component { + render() { + const { me, t } = this.props; + return ( +
+ +
+

+ +

+
+
+
+ + + + + + + + + + + + + + + + + + + +
{t("profile.username")}{me.name}
{t("profile.displayName")}{me.displayName}
{t("profile.mail")} + +
{t("profile.groups")} +
    + {me.groups.map((group) => { + return
  • {group}
  • ; + })} +
+
+
+
+ ); + } +} + +export default compose(translate("commons"))(ProfileInfo); diff --git a/scm-ui/src/modules/auth.js b/scm-ui/src/modules/auth.js index e9bccb8fbc..5d15107406 100644 --- a/scm-ui/src/modules/auth.js +++ b/scm-ui/src/modules/auth.js @@ -134,15 +134,6 @@ const callFetchMe = (link: string): Promise => { .get(link) .then(response => { return response.json(); - }) - .then(json => { - const { name, displayName, mail, _links } = json; - return { - name, - displayName, - mail, - _links - }; }); }; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java index 66eadaad7d..669a10143a 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java @@ -8,7 +8,6 @@ public class MapperModule extends AbstractModule { @Override protected void configure() { bind(UserDtoToUserMapper.class).to(Mappers.getMapper(UserDtoToUserMapper.class).getClass()); - bind(MeToUserDtoMapper.class).to(Mappers.getMapper(MeToUserDtoMapper.class).getClass()); bind(UserToUserDtoMapper.class).to(Mappers.getMapper(UserToUserDtoMapper.class).getClass()); bind(UserCollectionToDtoMapper.class); @@ -46,6 +45,7 @@ public class MapperModule extends AbstractModule { bind(MergeResultToDtoMapper.class).to(Mappers.getMapper(MergeResultToDtoMapper.class).getClass()); // no mapstruct required + bind(MeDtoFactory.class); bind(UIPluginDtoMapper.class); bind(UIPluginDtoCollectionMapper.class); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeDto.java new file mode 100644 index 0000000000..5488faca28 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeDto.java @@ -0,0 +1,26 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.HalRepresentation; +import de.otto.edison.hal.Links; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +@NoArgsConstructor +public class MeDto extends HalRepresentation { + + private String name; + private String displayName; + private String mail; + private List groups; + + @Override + @SuppressWarnings("squid:S1185") // We want to have this method available in this package + protected HalRepresentation add(Links links) { + return super.add(links); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeDtoFactory.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeDtoFactory.java new file mode 100644 index 0000000000..082db7fd94 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeDtoFactory.java @@ -0,0 +1,81 @@ +package sonia.scm.api.v2.resources; + +import com.google.common.collect.ImmutableList; +import de.otto.edison.hal.Links; +import org.apache.shiro.SecurityUtils; +import org.apache.shiro.subject.PrincipalCollection; +import org.apache.shiro.subject.Subject; +import sonia.scm.group.GroupNames; +import sonia.scm.user.User; +import sonia.scm.user.UserManager; +import sonia.scm.user.UserPermissions; + +import javax.inject.Inject; +import java.util.Collections; + +import static de.otto.edison.hal.Link.link; +import static de.otto.edison.hal.Links.linkingTo; + +public class MeDtoFactory extends LinkAppenderMapper { + + private final ResourceLinks resourceLinks; + private final UserManager userManager; + + @Inject + public MeDtoFactory(ResourceLinks resourceLinks, UserManager userManager) { + this.resourceLinks = resourceLinks; + this.userManager = userManager; + } + + public MeDto create() { + PrincipalCollection principals = getPrincipalCollection(); + + MeDto dto = new MeDto(); + + User user = principals.oneByType(User.class); + + mapUserProperties(user, dto); + mapGroups(principals, dto); + + appendLinks(user, dto); + return dto; + } + + private void mapGroups(PrincipalCollection principals, MeDto dto) { + Iterable groups = principals.oneByType(GroupNames.class); + if (groups == null) { + groups = Collections.emptySet(); + } + dto.setGroups(ImmutableList.copyOf(groups)); + } + + private void mapUserProperties(User user, MeDto dto) { + dto.setName(user.getName()); + dto.setDisplayName(user.getDisplayName()); + dto.setMail(user.getMail()); + } + + private PrincipalCollection getPrincipalCollection() { + Subject subject = SecurityUtils.getSubject(); + return subject.getPrincipals(); + } + + + private void appendLinks(User user, MeDto target) { + Links.Builder linksBuilder = linkingTo().self(resourceLinks.me().self()); + if (UserPermissions.delete(user).isPermitted()) { + linksBuilder.single(link("delete", resourceLinks.me().delete(target.getName()))); + } + if (UserPermissions.modify(user).isPermitted()) { + linksBuilder.single(link("update", resourceLinks.me().update(target.getName()))); + } + if (userManager.isTypeDefault(user) && UserPermissions.changePassword(user).isPermitted()) { + linksBuilder.single(link("password", resourceLinks.me().passwordChange())); + } + + appendLinks(new EdisonLinkAppender(linksBuilder), new Me(), user); + + target.add(linksBuilder.build()); + } + +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeResource.java index 20fc35923c..2c2e208893 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeResource.java @@ -3,14 +3,11 @@ package sonia.scm.api.v2.resources; import com.webcohesion.enunciate.metadata.rs.ResponseCode; import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; -import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.credential.PasswordService; -import sonia.scm.user.User; import sonia.scm.user.UserManager; import sonia.scm.web.VndMediaType; import javax.inject.Inject; -import javax.inject.Named; import javax.validation.Valid; import javax.ws.rs.Consumes; import javax.ws.rs.GET; @@ -28,20 +25,18 @@ import javax.ws.rs.core.UriInfo; */ @Path(MeResource.ME_PATH_V2) public class MeResource { - public static final String ME_PATH_V2 = "v2/me/"; - private final MeToUserDtoMapper meToUserDtoMapper; + static final String ME_PATH_V2 = "v2/me/"; - private final IdResourceManagerAdapter adapter; - private final PasswordService passwordService; + private final MeDtoFactory meDtoFactory; private final UserManager userManager; + private final PasswordService passwordService; @Inject - public MeResource(MeToUserDtoMapper meToUserDtoMapper, UserManager manager, PasswordService passwordService) { - this.meToUserDtoMapper = meToUserDtoMapper; - this.adapter = new IdResourceManagerAdapter<>(manager, User.class); + public MeResource(MeDtoFactory meDtoFactory, UserManager userManager, PasswordService passwordService) { + this.meDtoFactory = meDtoFactory; + this.userManager = userManager; this.passwordService = passwordService; - this.userManager = manager; } /** @@ -49,17 +44,15 @@ public class MeResource { */ @GET @Path("") - @Produces(VndMediaType.USER) - @TypeHint(UserDto.class) + @Produces(VndMediaType.ME) + @TypeHint(MeDto.class) @StatusCodes({ @ResponseCode(code = 200, condition = "success"), @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), @ResponseCode(code = 500, condition = "internal server error") }) public Response get(@Context Request request, @Context UriInfo uriInfo) { - - String id = (String) SecurityUtils.getSubject().getPrincipals().getPrimaryPrincipal(); - return adapter.get(id, meToUserDtoMapper::map); + return Response.ok(meDtoFactory.create()).build(); } /** @@ -75,7 +68,10 @@ public class MeResource { @TypeHint(TypeHint.NO_CONTENT.class) @Consumes(VndMediaType.PASSWORD_CHANGE) public Response changePassword(@Valid PasswordChangeDto passwordChange) { - userManager.changePasswordForLoggedInUser(passwordService.encryptPassword(passwordChange.getOldPassword()), passwordService.encryptPassword(passwordChange.getNewPassword())); + userManager.changePasswordForLoggedInUser( + passwordService.encryptPassword(passwordChange.getOldPassword()), + passwordService.encryptPassword(passwordChange.getNewPassword()) + ); return Response.noContent().build(); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeToUserDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeToUserDtoMapper.java deleted file mode 100644 index c6d98a826e..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeToUserDtoMapper.java +++ /dev/null @@ -1,45 +0,0 @@ -package sonia.scm.api.v2.resources; - -import de.otto.edison.hal.Links; -import org.mapstruct.AfterMapping; -import org.mapstruct.Mapper; -import org.mapstruct.MappingTarget; -import sonia.scm.user.User; -import sonia.scm.user.UserManager; -import sonia.scm.user.UserPermissions; - -import javax.inject.Inject; - -import static de.otto.edison.hal.Link.link; -import static de.otto.edison.hal.Links.linkingTo; - -@Mapper -public abstract class MeToUserDtoMapper extends UserToUserDtoMapper { - - @Inject - private UserManager userManager; - - @Inject - private ResourceLinks resourceLinks; - - - @Override - @AfterMapping - protected void appendLinks(User user, @MappingTarget UserDto target) { - Links.Builder linksBuilder = linkingTo().self(resourceLinks.me().self()); - if (UserPermissions.delete(user).isPermitted()) { - linksBuilder.single(link("delete", resourceLinks.me().delete(target.getName()))); - } - if (UserPermissions.modify(user).isPermitted()) { - linksBuilder.single(link("update", resourceLinks.me().update(target.getName()))); - } - if (userManager.isTypeDefault(user)) { - linksBuilder.single(link("password", resourceLinks.me().passwordChange())); - } - - appendLinks(new EdisonLinkAppender(linksBuilder), new Me(), user); - - target.add(linksBuilder.build()); - } - -} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeDtoFactoryTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeDtoFactoryTest.java new file mode 100644 index 0000000000..138387938b --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeDtoFactoryTest.java @@ -0,0 +1,186 @@ +package sonia.scm.api.v2.resources; + +import org.apache.shiro.subject.PrincipalCollection; +import org.apache.shiro.subject.Subject; +import org.apache.shiro.util.ThreadContext; +import org.assertj.core.util.Lists; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import sonia.scm.group.GroupNames; +import sonia.scm.user.User; +import sonia.scm.user.UserManager; +import sonia.scm.user.UserPermissions; +import sonia.scm.user.UserTestData; + +import java.net.URI; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class MeDtoFactoryTest { + + private final URI baseUri = URI.create("https://scm.hitchhiker.com/scm/"); + + @Mock + private UserManager userManager; + + @Mock + private Subject subject; + + private MeDtoFactory meDtoFactory; + + @BeforeEach + void setUpContext() { + ThreadContext.bind(subject); + ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); + meDtoFactory = new MeDtoFactory(resourceLinks, userManager); + } + + @AfterEach + void unbindSubject() { + ThreadContext.unbindSubject(); + } + + @Test + void shouldCreateMeDtoFromUser() { + prepareSubject(UserTestData.createTrillian()); + + MeDto dto = meDtoFactory.create(); + assertThat(dto.getName()).isEqualTo("trillian"); + assertThat(dto.getDisplayName()).isEqualTo("Tricia McMillan"); + assertThat(dto.getMail()).isEqualTo("tricia.mcmillan@hitchhiker.com"); + } + + @Test + void shouldCreateMeDtoWithEmptyGroups() { + prepareSubject(UserTestData.createTrillian()); + MeDto dto = meDtoFactory.create(); + assertThat(dto.getGroups()).isEmpty(); + } + + @Test + void shouldCreateMeDtoWithGroups() { + prepareSubject(UserTestData.createTrillian(), "HeartOfGold", "Puzzle42"); + MeDto dto = meDtoFactory.create(); + assertThat(dto.getGroups()).containsOnly("HeartOfGold", "Puzzle42"); + } + + private void prepareSubject(User user, String... groups) { + PrincipalCollection collection = mock(PrincipalCollection.class); + when(subject.getPrincipals()).thenReturn(collection); + when(collection.oneByType(any(Class.class))).then(ic -> { + Class type = ic.getArgument(0); + if (type.isAssignableFrom(User.class)) { + return user; + } else if (type.isAssignableFrom(GroupNames.class)) { + return new GroupNames(Lists.newArrayList(groups)); + } else { + return null; + } + }); + } + + @Test + void shouldAppendSelfLink() { + prepareSubject(UserTestData.createTrillian()); + + MeDto dto = meDtoFactory.create(); + assertThat(dto.getLinks().getLinkBy("self").get().getHref()).isEqualTo("https://scm.hitchhiker.com/scm/v2/me/"); + } + + @Test + void shouldAppendDeleteLink() { + prepareSubject(UserTestData.createTrillian()); + when(subject.isPermitted("user:delete:trillian")).thenReturn(true); + + MeDto dto = meDtoFactory.create(); + assertThat(dto.getLinks().getLinkBy("delete").get().getHref()).isEqualTo("https://scm.hitchhiker.com/scm/v2/users/trillian"); + } + + @Test + void shouldNotAppendDeleteLink() { + prepareSubject(UserTestData.createTrillian()); + + MeDto dto = meDtoFactory.create(); + assertThat(dto.getLinks().getLinkBy("delete")).isNotPresent(); + } + + @Test + void shouldAppendUpdateLink() { + prepareSubject(UserTestData.createTrillian()); + when(subject.isPermitted("user:modify:trillian")).thenReturn(true); + + MeDto dto = meDtoFactory.create(); + assertThat(dto.getLinks().getLinkBy("update").get().getHref()).isEqualTo("https://scm.hitchhiker.com/scm/v2/users/trillian"); + } + + @Test + void shouldNotAppendUpdateLink() { + prepareSubject(UserTestData.createTrillian()); + + MeDto dto = meDtoFactory.create(); + assertThat(dto.getLinks().getLinkBy("update")).isNotPresent(); + } + + @Test + void shouldGetPasswordLinkOnlyForDefaultUserType() { + User user = UserTestData.createTrillian(); + prepareSubject(user); + + when(subject.isPermitted("user:changePassword:trillian")).thenReturn(true); + when(userManager.isTypeDefault(user)).thenReturn(true); + + MeDto dto = meDtoFactory.create(); + assertThat(dto.getLinks().getLinkBy("password").get().getHref()).isEqualTo("https://scm.hitchhiker.com/scm/v2/me/password"); + } + + @Test + void shouldNotGetPasswordLinkWithoutPermision() { + User user = UserTestData.createTrillian(); + prepareSubject(user); + + when(userManager.isTypeDefault(user)).thenReturn(true); + + MeDto dto = meDtoFactory.create(); + assertThat(dto.getLinks().getLinkBy("password")).isNotPresent(); + } + + @Test + void shouldNotGetPasswordLinkForNonDefaultUsers() { + User user = UserTestData.createTrillian(); + prepareSubject(user); + + when(subject.isPermitted("user:changePassword:trillian")).thenReturn(true); + + MeDto dto = meDtoFactory.create(); + assertThat(dto.getLinks().getLinkBy("password")).isNotPresent(); + } + + @Test + void shouldAppendLinks() { + prepareSubject(UserTestData.createTrillian()); + + LinkEnricherRegistry registry = new LinkEnricherRegistry(); + meDtoFactory.setRegistry(registry); + + registry.register(Me.class, (ctx, appender) -> { + User user = ctx.oneRequireByType(User.class); + appender.appendOne("profile", "http://hitchhiker.com/users/" + user.getName()); + }); + + MeDto dto = meDtoFactory.create(); + assertThat(dto.getLinks().getLinkBy("profile").get().getHref()).isEqualTo("http://hitchhiker.com/users/trillian"); + } + + +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeResourceTest.java index 454e24aa92..052a059959 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeResourceTest.java @@ -2,12 +2,14 @@ package sonia.scm.api.v2.resources; import com.github.sdorra.shiro.ShiroRule; import com.github.sdorra.shiro.SubjectAware; +import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.credential.PasswordService; +import org.apache.shiro.subject.PrincipalCollection; +import org.apache.shiro.subject.Subject; import org.jboss.resteasy.core.Dispatcher; import org.jboss.resteasy.mock.MockHttpRequest; import org.jboss.resteasy.mock.MockHttpResponse; import org.junit.Before; -import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.mockito.ArgumentCaptor; @@ -19,7 +21,6 @@ import sonia.scm.user.User; import sonia.scm.user.UserManager; import sonia.scm.web.VndMediaType; -import javax.lang.model.util.Types; import javax.servlet.http.HttpServletResponse; import java.net.URI; import java.net.URISyntaxException; @@ -27,11 +28,7 @@ import java.net.URISyntaxException; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; import static org.mockito.MockitoAnnotations.initMocks; import static sonia.scm.api.v2.resources.DispatcherMock.createDispatcher; @@ -57,7 +54,7 @@ public class MeResourceTest { private UserManager userManager; @InjectMocks - private MeToUserDtoMapperImpl userToDtoMapper; + private MeDtoFactory meDtoFactory; private ArgumentCaptor userCaptor = ArgumentCaptor.forClass(User.class); @@ -66,7 +63,7 @@ public class MeResourceTest { private User originalUser; @Before - public void prepareEnvironment() throws Exception { + public void prepareEnvironment() { initMocks(this); originalUser = createDummyUser("trillian"); when(userManager.create(userCaptor.capture())).thenAnswer(invocation -> invocation.getArguments()[0]); @@ -74,17 +71,18 @@ public class MeResourceTest { doNothing().when(userManager).delete(userCaptor.capture()); when(userManager.isTypeDefault(userCaptor.capture())).thenCallRealMethod(); when(userManager.getDefaultType()).thenReturn("xml"); - MeResource meResource = new MeResource(userToDtoMapper, userManager, passwordService); + MeResource meResource = new MeResource(meDtoFactory, userManager, passwordService); when(uriInfo.getApiRestUri()).thenReturn(URI.create("/")); when(scmPathInfoStore.get()).thenReturn(uriInfo); dispatcher = createDispatcher(meResource); } @Test - @SubjectAware(username = "trillian", password = "secret") public void shouldReturnCurrentlyAuthenticatedUser() throws URISyntaxException { + applyUserToSubject(originalUser); + MockHttpRequest request = MockHttpRequest.get("/" + MeResource.ME_PATH_V2); - request.accept(VndMediaType.USER); + request.accept(VndMediaType.ME); MockHttpResponse response = new MockHttpResponse(); dispatcher.invoke(request, response); @@ -95,8 +93,17 @@ public class MeResourceTest { assertTrue(response.getContentAsString().contains("\"delete\":{\"href\":\"/v2/users/trillian\"}")); } + private void applyUserToSubject(User user) { + // use spy here to keep applied permissions from ShiroRule + Subject subject = spy(SecurityUtils.getSubject()); + PrincipalCollection collection = mock(PrincipalCollection.class); + when(collection.getPrimaryPrincipal()).thenReturn(user.getName()); + when(subject.getPrincipals()).thenReturn(collection); + when(collection.oneByType(User.class)).thenReturn(user); + shiro.setSubject(subject); + } + @Test - @SubjectAware(username = "trillian", password = "secret") public void shouldEncryptPasswordBeforeChanging() throws Exception { String newPassword = "pwd123"; String encryptedNewPassword = "encrypted123"; @@ -124,7 +131,6 @@ public class MeResourceTest { } @Test - @SubjectAware(username = "trillian", password = "secret") public void shouldGet400OnMissingOldPassword() throws Exception { originalUser.setType("not an xml type"); String newPassword = "pwd123"; @@ -141,7 +147,6 @@ public class MeResourceTest { } @Test - @SubjectAware(username = "trillian", password = "secret") public void shouldGet400OnMissingEmptyPassword() throws Exception { String newPassword = "pwd123"; String oldPassword = ""; @@ -158,7 +163,6 @@ public class MeResourceTest { } @Test - @SubjectAware(username = "trillian", password = "secret") public void shouldMapExceptionFromManager() throws Exception { String newPassword = "pwd123"; String oldPassword = "secret"; diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeToUserDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeToUserDtoMapperTest.java deleted file mode 100644 index 5aa940304e..0000000000 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeToUserDtoMapperTest.java +++ /dev/null @@ -1,151 +0,0 @@ -package sonia.scm.api.v2.resources; - -import org.apache.shiro.subject.Subject; -import org.apache.shiro.subject.support.SubjectThreadState; -import org.apache.shiro.util.ThreadContext; -import org.apache.shiro.util.ThreadState; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import sonia.scm.user.User; -import sonia.scm.user.UserManager; -import sonia.scm.user.UserTestData; - -import java.net.URI; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import static org.mockito.MockitoAnnotations.initMocks; - -public class MeToUserDtoMapperTest { - - private final URI baseUri = URI.create("http://example.com/base/"); - @SuppressWarnings("unused") // Is injected - private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); - - @Mock - private UserManager userManager; - - @InjectMocks - private MeToUserDtoMapperImpl mapper; - - private final Subject subject = mock(Subject.class); - private final ThreadState subjectThreadState = new SubjectThreadState(subject); - - private URI expectedBaseUri; - private URI expectedUserBaseUri; - - @Before - public void init() { - initMocks(this); - when(userManager.getDefaultType()).thenReturn("xml"); - expectedBaseUri = baseUri.resolve(MeResource.ME_PATH_V2 + "/"); - expectedUserBaseUri = baseUri.resolve(UserRootResource.USERS_PATH_V2 + "/"); - subjectThreadState.bind(); - ThreadContext.bind(subject); - } - - @After - public void unbindSubject() { - ThreadContext.unbindSubject(); - } - - @Test - public void shouldMapTheUpdateLink() { - User user = createDefaultUser(); - when(subject.isPermitted("user:modify:abc")).thenReturn(true); - - UserDto userDto = mapper.map(user); - assertEquals("expected update link", expectedUserBaseUri.resolve("abc").toString(), userDto.getLinks().getLinkBy("update").get().getHref()); - - when(subject.isPermitted("user:modify:abc")).thenReturn(false); - userDto = mapper.map(user); - assertFalse("expected no update link", userDto.getLinks().getLinkBy("update").isPresent()); - } - - @Test - public void shouldMapTheSelfLink() { - User user = createDefaultUser(); - when(subject.isPermitted("user:modify:abc")).thenReturn(true); - - UserDto userDto = mapper.map(user); - assertEquals("expected self link", expectedBaseUri.toString(), userDto.getLinks().getLinkBy("self").get().getHref()); - - } - - @Test - public void shouldMapTheDeleteLink() { - User user = createDefaultUser(); - when(subject.isPermitted("user:delete:abc")).thenReturn(true); - - UserDto userDto = mapper.map(user); - assertEquals("expected update link", expectedUserBaseUri.resolve("abc").toString(), userDto.getLinks().getLinkBy("delete").get().getHref()); - - when(subject.isPermitted("user:delete:abc")).thenReturn(false); - userDto = mapper.map(user); - assertFalse("expected no delete link", userDto.getLinks().getLinkBy("delete").isPresent()); - } - - @Test - public void shouldGetPasswordLinkOnlyForDefaultUserType() { - User user = createDefaultUser(); - when(subject.isPermitted("user:modify:abc")).thenReturn(true); - when(userManager.isTypeDefault(eq(user))).thenReturn(true); - - UserDto userDto = mapper.map(user); - - assertEquals("expected password link with modify permission", expectedBaseUri.resolve("password").toString(), userDto.getLinks().getLinkBy("password").get().getHref()); - - when(subject.isPermitted("user:modify:abc")).thenReturn(false); - userDto = mapper.map(user); - assertEquals("expected password link on mission modify permission", expectedBaseUri.resolve("password").toString(), userDto.getLinks().getLinkBy("password").get().getHref()); - - when(userManager.isTypeDefault(eq(user))).thenReturn(false); - - userDto = mapper.map(user); - - assertFalse("expected no password link", userDto.getLinks().getLinkBy("password").isPresent()); - } - - - @Test - public void shouldGetEmptyPasswordProperty() { - User user = createDefaultUser(); - user.setPassword("myHighSecurePassword"); - when(subject.isPermitted("user:modify:abc")).thenReturn(true); - - UserDto userDto = mapper.map(user); - - assertThat(userDto.getPassword()).as("hide password for the me resource").isBlank(); - } - - @Test - public void shouldAppendLinks() { - LinkEnricherRegistry registry = new LinkEnricherRegistry(); - registry.register(Me.class, (ctx, appender) -> { - User user = ctx.oneRequireByType(User.class); - appender.appendOne("profile", "http://hitchhiker.com/users/" + user.getName()); - }); - mapper.setRegistry(registry); - - User trillian = UserTestData.createTrillian(); - UserDto dto = mapper.map(trillian); - - assertEquals("http://hitchhiker.com/users/trillian", dto.getLinks().getLinkBy("profile").get().getHref()); - } - - private User createDefaultUser() { - User user = new User(); - user.setName("abc"); - user.setCreationDate(1L); - return user; - } - - -}