Merged in feature/consolidate_admin_permissions (pull request #219)

Feature/consolidate admin permissions
This commit is contained in:
Sebastian Sdorra
2019-03-21 12:17:01 +00:00
64 changed files with 691 additions and 2639 deletions

View File

@@ -1,181 +0,0 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm;
//~--- non-JDK imports --------------------------------------------------------
import sonia.scm.config.ScmConfiguration;
/**
* Configuration object for a SCM-Manager
* client (WebInterface, RestClient, ...).
*
* @author Sebastian Sdorra
*/
public class ScmClientConfig
{
/**
* Constructs {@link ScmClientConfig} object
*
*/
public ScmClientConfig() {}
/**
* Constructs {@link ScmClientConfig} object
*
*
* @param configuration SCM-Manager main configuration
* @since 1.14
*/
public ScmClientConfig(ScmConfiguration configuration)
{
this.dateFormat = configuration.getDateFormat();
this.disableGroupingGrid = configuration.isDisableGroupingGrid();
this.enableRepositoryArchive = configuration.isEnableRepositoryArchive();
}
/**
* Constructs {@link ScmClientConfig} object
*
*
* @param dateFormat
*/
public ScmClientConfig(String dateFormat)
{
this.dateFormat = dateFormat;
}
/**
* Constructs {@link ScmClientConfig} object
*
* @since 1.9
*
* @param dateFormat
* @param disableGroupingGrid true to disable repository grouping
*/
public ScmClientConfig(String dateFormat, boolean disableGroupingGrid)
{
this.dateFormat = dateFormat;
this.disableGroupingGrid = disableGroupingGrid;
}
//~--- get methods ----------------------------------------------------------
/**
* Returns the date format for the user interface. This format is a
* JavaScript date format.
*
* @see <a target="_blank" href="http://jacwright.com/projects/javascript/date_format">Date Format</a>
* @return JavaScript date format
*/
public String getDateFormat()
{
return dateFormat;
}
/**
* Returns true if the grouping of repositories is disabled.
*
* @since 1.9
*
* @return true if the grouping of repositories is disabled
*/
public boolean isDisableGroupingGrid()
{
return disableGroupingGrid;
}
/**
* Returns true if the repository archive is disabled.
*
*
* @return true if the repository archive is disabled
* @since 1.14
*/
public boolean isEnableRepositoryArchive()
{
return enableRepositoryArchive;
}
//~--- set methods ----------------------------------------------------------
/**
* Setter for the date format
*
*
*
* @param dateFormat - JavaScript date format
*/
public void setDateFormat(String dateFormat)
{
this.dateFormat = dateFormat;
}
/**
* Enables or disables the grouping of repositories.
*
* @since 1.9
*
*
* @param disableGroupingGrid
*/
public void setDisableGroupingGrid(boolean disableGroupingGrid)
{
this.disableGroupingGrid = disableGroupingGrid;
}
/**
* Enable or disable the repository archive. Default is disabled.
*
*
* @param enableRepositoryArchive true to disable the repository archive
* @since 1.14
*/
public void setEnableRepositoryArchive(boolean enableRepositoryArchive)
{
this.enableRepositoryArchive = enableRepositoryArchive;
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private String dateFormat;
/** Field description */
private boolean enableRepositoryArchive = true;
/** Field description */
private boolean disableGroupingGrid = true;
}

View File

@@ -1,237 +0,0 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm;
//~--- non-JDK imports --------------------------------------------------------
import sonia.scm.repository.RepositoryType;
import sonia.scm.security.PermissionDescriptor;
import sonia.scm.user.User;
//~--- JDK imports ------------------------------------------------------------
import java.util.Collection;
import java.util.List;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
/**
* This class represents the current state of the SCM-Manager.
*
* @author Sebastian Sdorra
*/
@XmlRootElement(name = "state")
@XmlAccessorType(XmlAccessType.FIELD)
public final class ScmState
{
/**
* Constructs {@link ScmState} object.
* This constructor is required by JAXB.
*
*/
ScmState() {}
/**
* Constructs {@link ScmState} object.
*
*
* @param version scm-manager version
* @param user current user
* @param groups groups of the current user
* @param token authentication token
* @param repositoryTypes available repository types
* @param defaultUserType default user type
* @param clientConfig client configuration
* @param availablePermissions list of available permissions
*
* @since 2.0.0
*/
public ScmState(String version, User user, Collection<String> groups,
String token, Collection<RepositoryType> repositoryTypes, String defaultUserType,
ScmClientConfig clientConfig, Collection<PermissionDescriptor> availablePermissions)
{
this.version = version;
this.user = user;
this.groups = groups;
this.token = token;
this.repositoryTypes = repositoryTypes;
this.clientConfig = clientConfig;
this.defaultUserType = defaultUserType;
this.availablePermissions = availablePermissions;
}
//~--- get methods ----------------------------------------------------------
/**
* Returns a list of available global permissions.
*
*
* @return available global permissions
* @since 1.31
*/
public Collection<PermissionDescriptor> getAvailablePermissions()
{
return availablePermissions;
}
/**
* Returns configuration for SCM-Manager clients.
*
*
* @return configuration for SCM-Manager clients
*/
public ScmClientConfig getClientConfig()
{
return clientConfig;
}
/**
* Returns the default user type
*
*
* @return default user type
*
* @since 1.14
*/
public String getDefaultUserType()
{
return defaultUserType;
}
/**
* Returns a {@link java.util.Collection} of groups names which are associated
* to the current user.
*
*
* @return a {@link java.util.Collection} of groups names
*/
public Collection<String> getGroups()
{
return groups;
}
/**
* Returns all available repository types.
*
*
* @return all available repository types
*/
public Collection<RepositoryType> getRepositoryTypes()
{
return repositoryTypes;
}
/**
* Returns authentication token or {@code null}.
*
*
* @return authentication token or {@code null}
*
* @since 2.0.0
*/
public String getToken()
{
return token;
}
/**
* Returns the current logged in user.
*
*
* @return current logged in user
*/
public User getUser()
{
return user;
}
/**
* Returns the version of the SCM-Manager.
*
*
* @return version of the SCM-Manager
*/
public String getVersion()
{
return version;
}
/**
* Returns true if the request was successful.
* This method is required by extjs.
*
* @return true if the request was successful
*/
public boolean isSuccess()
{
return success;
}
//~--- fields ---------------------------------------------------------------
/** marker for extjs */
private final boolean success = true;
/** authentication token */
private String token;
/**
* Avaliable global permission
* @since 1.31
*/
private Collection<PermissionDescriptor> availablePermissions;
/** Field description */
private ScmClientConfig clientConfig;
/** Field description */
private String defaultUserType;
/** Field description */
private Collection<String> groups;
/** Field description */
@XmlElement(name = "repositoryTypes")
private Collection<RepositoryType> repositoryTypes;
/** Field description */
private User user;
/** Field description */
private String version;
}

View File

@@ -1,173 +0,0 @@
/**
* Copyright (c) 2014, Sebastian Sdorra All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer. 2. Redistributions in
* binary form must reproduce the above copyright notice, this list of
* conditions and the following disclaimer in the documentation and/or other
* materials provided with the distribution. 3. Neither the name of SCM-Manager;
* nor the names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.collect.ImmutableList;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.group.GroupNames;
import sonia.scm.repository.RepositoryManager;
import sonia.scm.security.AuthorizationCollector;
import sonia.scm.security.PermissionDescriptor;
import sonia.scm.security.Role;
import sonia.scm.security.SecuritySystem;
import sonia.scm.user.User;
import sonia.scm.user.UserManager;
//~--- JDK imports ------------------------------------------------------------
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import javax.inject.Inject;
/**
* Factory to create {@link ScmState}.
*
* @author Sebastian Sdorra
* @since 2.0.0
*/
public final class ScmStateFactory
{
/**
* Constructs a new {@link ScmStateFactory}.
*
*
* @param contextProvider context provider
* @param configuration configuration
* @param repositoryManger repository manager
* @param userManager user manager
* @param securitySystem security system
*/
@Inject
public ScmStateFactory(SCMContextProvider contextProvider,
ScmConfiguration configuration, RepositoryManager repositoryManger,
UserManager userManager, SecuritySystem securitySystem)
{
this.contextProvider = contextProvider;
this.configuration = configuration;
this.repositoryManger = repositoryManger;
this.userManager = userManager;
this.securitySystem = securitySystem;
}
//~--- methods --------------------------------------------------------------
/**
* Returns anonymous state.
*
*
* @return anonymous state
*/
@SuppressWarnings("unchecked")
public ScmState createAnonymousState()
{
return createState(SCMContext.ANONYMOUS, Collections.EMPTY_LIST, null, Collections.EMPTY_LIST);
}
/**
* Creates an state from the given subject.
*
*
* @param subject subject
*
* @return state from subject
*/
public ScmState createState(Subject subject)
{
return createState(subject, null);
}
/**
* Creates an state from the given subject and authentication token.
*
*
* @param subject subject
* @param token authentication token
*
* @return state from subject and authentication token
*/
@SuppressWarnings("unchecked")
public ScmState createState(Subject subject, String token)
{
PrincipalCollection collection = subject.getPrincipals();
User user = collection.oneByType(User.class);
GroupNames groups = collection.oneByType(GroupNames.class);
Collection<PermissionDescriptor> ap = Collections.EMPTY_LIST;
if (subject.hasRole(Role.ADMIN))
{
ap = securitySystem.getAvailablePermissions();
}
return createState(user, groups.getCollection(), token, ap);
}
private ScmState createState(User user, Collection<String> groups,
String token,
Collection<PermissionDescriptor> availablePermissions)
{
User u = user.clone();
// do not return password on authentication
u.setPassword(null);
return new ScmState(contextProvider.getVersion(), u, groups, token,
repositoryManger.getConfiguredTypes(), userManager.getDefaultType(),
new ScmClientConfig(configuration), availablePermissions);
}
//~--- fields ---------------------------------------------------------------
/** configuration */
private final ScmConfiguration configuration;
/** context provider */
private final SCMContextProvider contextProvider;
/** repository manager */
private final RepositoryManager repositoryManger;
/** security system */
private final SecuritySystem securitySystem;
/** user manager */
private final UserManager userManager;
}

View File

@@ -95,16 +95,6 @@ public class ScmConfiguration implements Configuration {
@SuppressWarnings("WeakerAccess") // This might be needed for permission checking
public static final String PERMISSION = "global";
@XmlElement(name = "admin-groups")
@XmlJavaTypeAdapter(XmlSetStringAdapter.class)
private Set<String> adminGroups;
@XmlElement(name = "admin-users")
@XmlJavaTypeAdapter(XmlSetStringAdapter.class)
private Set<String> adminUsers;
@XmlElement(name = "base-url")
private String baseUrl;
@@ -211,8 +201,6 @@ public class ScmConfiguration implements Configuration {
this.dateFormat = other.dateFormat;
this.pluginUrl = other.pluginUrl;
this.anonymousAccessEnabled = other.anonymousAccessEnabled;
this.adminUsers = other.adminUsers;
this.adminGroups = other.adminGroups;
this.enableProxy = other.enableProxy;
this.proxyPort = other.proxyPort;
this.proxyServer = other.proxyServer;
@@ -230,14 +218,6 @@ public class ScmConfiguration implements Configuration {
this.namespaceStrategy = other.namespaceStrategy;
}
public Set<String> getAdminGroups() {
return adminGroups;
}
public Set<String> getAdminUsers() {
return adminUsers;
}
/**
* Returns the complete base url of the scm-manager including the context path.
* For example http://localhost:8080/scm
@@ -381,14 +361,6 @@ public class ScmConfiguration implements Configuration {
return skipFailedAuthenticators;
}
public void setAdminGroups(Set<String> adminGroups) {
this.adminGroups = adminGroups;
}
public void setAdminUsers(Set<String> adminUsers) {
this.adminUsers = adminUsers;
}
public void setAnonymousAccessEnabled(boolean anonymousAccessEnabled) {
this.anonymousAccessEnabled = anonymousAccessEnabled;
}

View File

@@ -50,6 +50,7 @@ import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
//~--- JDK imports ------------------------------------------------------------
@@ -195,6 +196,7 @@ public class Group extends BasicPropertiesAware
group.setMembers(members);
group.setType(type);
group.setDescription(description);
group.setExternal(external);
}
/**
@@ -224,6 +226,7 @@ public class Group extends BasicPropertiesAware
&& Objects.equal(description, other.description)
&& Objects.equal(members, other.members)
&& Objects.equal(type, other.type)
&& Objects.equal(external, other.external)
&& Objects.equal(creationDate, other.creationDate)
&& Objects.equal(lastModified, other.lastModified)
&& Objects.equal(properties, other.properties);
@@ -270,6 +273,7 @@ public class Group extends BasicPropertiesAware
.add("description", description)
.add("members", members)
.add("type", type)
.add("external", external)
.add("creationDate", creationDate)
.add("lastModified", lastModified)
.add("properties", properties)
@@ -339,8 +343,9 @@ public class Group extends BasicPropertiesAware
*/
public List<String> getMembers()
{
if (members == null)
{
if (external) {
return Collections.emptyList();
} else if (members == null) {
members = Lists.newArrayList();
}
@@ -370,6 +375,15 @@ public class Group extends BasicPropertiesAware
return type;
}
/**
* Returns {@code true} if the members of the groups managed external of scm-manager.
*
* @return {@code true} if the group is an external group
*/
public boolean isExternal() {
return external;
}
/**
* Returns true if the member is a member of this group.
*
@@ -463,8 +477,21 @@ public class Group extends BasicPropertiesAware
this.type = type;
}
/**
* {@code true} to mark the group as external.
*
* @param {@code true} for a external group
*/
public void setExternal(boolean external)
{
this.external = external;
}
//~--- fields ---------------------------------------------------------------
/** external group */
private boolean external = false;
/** timestamp of the creation date of this group */
private Long creationDate;

View File

@@ -41,9 +41,6 @@ package sonia.scm.security;
public final class Role
{
/** Field description */
public static final String ADMIN = "admin";
/** Field description */
public static final String USER = "user";

View File

@@ -157,12 +157,6 @@ public class User extends BasicPropertiesAware implements Principal, ModelObject
{
boolean result = false;
if (user.isAdmin() != admin)
{
result = true;
user.setAdmin(admin);
}
if (user.isActive() != active)
{
result = true;
@@ -229,7 +223,6 @@ public class User extends BasicPropertiesAware implements Principal, ModelObject
&& Objects.equal(displayName, other.displayName)
&& Objects.equal(mail, other.mail)
&& Objects.equal(type, other.type)
&& Objects.equal(admin, other.admin)
&& Objects.equal(active, other.active)
&& Objects.equal(password, other.password)
&& Objects.equal(creationDate, other.creationDate)
@@ -246,7 +239,7 @@ public class User extends BasicPropertiesAware implements Principal, ModelObject
@Override
public int hashCode()
{
return Objects.hashCode(name, displayName, mail, type, admin, password,
return Objects.hashCode(name, displayName, mail, type, password,
active, creationDate, lastModified, properties);
}
@@ -269,7 +262,6 @@ public class User extends BasicPropertiesAware implements Principal, ModelObject
.add("displayName",displayName)
.add("mail", mail)
.add("password", pwd)
.add("admin", admin)
.add("type", type)
.add("active", active)
.add("creationDate", creationDate)
@@ -385,17 +377,6 @@ public class User extends BasicPropertiesAware implements Principal, ModelObject
return active;
}
/**
* Method description
*
*
* @return
*/
public boolean isAdmin()
{
return admin;
}
/**
* Method description
*
@@ -424,17 +405,6 @@ public class User extends BasicPropertiesAware implements Principal, ModelObject
this.active = active;
}
/**
* Method description
*
*
* @param admin
*/
public void setAdmin(boolean admin)
{
this.admin = admin;
}
/**
* Method description
*
@@ -518,9 +488,6 @@ public class User extends BasicPropertiesAware implements Principal, ModelObject
/** Field description */
private boolean active = true;
/** Field description */
private boolean admin = false;
/** Field description */
private Long creationDate;

View File

@@ -1,41 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!--
Copyright (c) 2010, Sebastian Sdorra
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of SCM-Manager; nor the names of its
contributors may be used to endorse or promote products derived from this
software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
http://bitbucket.org/sdorra/scm-manager
-->
<users>
<name>scmadmin</name>
<displayName>SCM Administrator</displayName>
<mail>scm-admin@scm-manager.org</mail>
<password>$shiro1$SHA-512$8192$$yrNahBVDa4Gz+y5gat4msdjyvjtHlVE+N5nTl4WIDhtBFwhSIib13mKJt1sWmVqgHDWi3VwX7fkdkJ2+WToTbw==</password>
<admin>true</admin>
<type>xml</type>
</users>

View File

@@ -1,49 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (c) 2010, Sebastian Sdorra
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of SCM-Manager; nor the names of its
contributors may be used to endorse or promote products derived from this
software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
http://bitbucket.org/sdorra/scm-manager
-->
<!--
Document : anonymous-account.xml
Created on : December 30, 2010, 12:44 PM
Author : sdorra
Description:
Purpose of the document follows.
-->
<users>
<name>anonymous</name>
<displayName>SCM Anonymous</displayName>
<mail>scm-anonymous@scm-manager.org</mail>
<admin>false</admin>
<type>xml</type>
</users>

View File

@@ -27,7 +27,6 @@ public class UserITCase {
.assertStatusCode(200)
.requestUser(newUser)
.assertStatusCode(200)
.assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE))
.assertPassword(Assert::assertNull)
.requestChangePassword(newPassword)
.assertStatusCode(204);
@@ -36,7 +35,6 @@ public class UserITCase {
.requestIndexResource(newUser, newPassword)
.assertStatusCode(200)
.requestUser(newUser)
.assertAdmin(isAdmin -> assertThat(isAdmin).isEqualTo(Boolean.TRUE))
.assertPassword(Assert::assertNull);
}
@@ -52,7 +50,6 @@ public class UserITCase {
.assertStatusCode(200)
.requestUser(newUser)
.assertStatusCode(200)
.assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE)) // the user anonymous is not an admin
.assertPassword(Assert::assertNull)
.requestChangePassword(newPassword) // the oldPassword is not needed in the user resource
.assertStatusCode(204);
@@ -96,7 +93,6 @@ public class UserITCase {
.assertStatusCode(200)
.requestUser(newUser)
.assertStatusCode(200)
.assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE))
.assertPassword(Assert::assertNull)
.assertType(s -> assertThat(s).isEqualTo(type))
.assertPasswordLinkDoesNotExists();

View File

@@ -338,18 +338,10 @@ public class ScmRequests {
return assertSingleProperty(assertType, "type");
}
public UserResponse<PREV> assertAdmin(Consumer<Boolean> assertAdmin) {
return assertSingleProperty(assertAdmin, "admin");
}
public UserResponse<PREV> assertPasswordLinkDoesNotExists() {
return assertPropertyPathDoesNotExists(LINKS_PASSWORD_HREF);
}
public UserResponse<PREV> assertPasswordLinkExists() {
return assertPropertyPathExists(LINKS_PASSWORD_HREF);
}
public ChangePasswordResponse<UserResponse> requestChangePassword(String newPassword) {
return new ChangePasswordResponse<>(applyPUTRequestFromLink(super.response, LINKS_PASSWORD_HREF, VndMediaType.PASSWORD_OVERWRITE, createPasswordChangeJson(null, newPassword)), this);
}

View File

@@ -74,9 +74,27 @@ public class TestData {
.append(" }").toString())
.post(getUsersUrl())
.then()
.statusCode(HttpStatus.SC_CREATED)
;
.statusCode(HttpStatus.SC_CREATED);
if (isAdmin) {
assignAdminPermissions(username);
}
}
public static void assignAdminPermissions(String username) {
LOG.info("assign admin permissions to user {}", username);
given(VndMediaType.PERMISSION_COLLECTION)
.when()
.body("{'permissions': ['*']}".replaceAll("'", "\""))
.put(getPermissionUrl(username))
.then()
.statusCode(HttpStatus.SC_NO_CONTENT);
}
private static URI getPermissionUrl(String username) {
return RestUtil.createResourceUrl(String.format("users/%s/permissions", username));
}
public static void createGroup(String groupName, String desc) {
LOG.info("create group with group name: {} and description {}", groupName, desc);
given(VndMediaType.GROUP)

View File

@@ -117,7 +117,6 @@ public final class MockUtil
when(subject.isPermittedAll(anyCollectionOf(Permission.class))).thenReturn(
Boolean.TRUE);
when(subject.isPermittedAll()).thenReturn(Boolean.TRUE);
when(subject.hasRole(Role.ADMIN)).thenReturn(Boolean.TRUE);
when(subject.hasRole(Role.USER)).thenReturn(Boolean.TRUE);
PrincipalCollection collection = mock(PrincipalCollection.class);

View File

@@ -63,4 +63,4 @@
]
]
}
}
}

View File

@@ -33,4 +33,4 @@
]
]
}
}
}

View File

@@ -12,8 +12,6 @@ export type Config = {
disableGroupingGrid: boolean,
dateFormat: string,
anonymousAccessEnabled: boolean,
adminGroups: string[],
adminUsers: string[],
baseUrl: string,
forceBaseUrl: boolean,
loginAttemptLimit: number,

View File

@@ -10,6 +10,7 @@ export type Group = Collection & {
name: string,
description: string,
type: string,
external: boolean,
members: string[],
_embedded: {
members: Member[]

View File

@@ -6,7 +6,6 @@ export type User = {
name: string,
mail: string,
password: string,
admin: boolean,
active: boolean,
type?: string,
creationDate?: string,

View File

@@ -30,19 +30,6 @@
"base-url": "Base URL",
"force-base-url": "Base URL erzwingen"
},
"admin-settings": {
"name": "Administrations Einstellungen",
"admin-groups": "Admin Gruppen",
"admin-users": "Admin Benutzer",
"remove-group-button": "Admin Group löschen",
"remove-user-button": "Admin Benutzer löschen",
"add-group-error": "Der eingegebene Gruppenname ist ungültig",
"add-group-textfield": "Neue Gruppe mit Administrationsrechten hinzufügen",
"add-group-button": "Admin Gruppe hinzufügen",
"add-user-error": "Der eingegebene Benutzername ist ungültig",
"add-user-textfield": "Neuen Benutzer mit Administrationsrechten hinzufügen",
"add-user-button": "Admin Benutzer hinzufügen"
},
"login-attempt": {
"name": "Anmeldeversuche",
"login-attempt-limit": "Limit für Anmeldeversuche",

View File

@@ -5,6 +5,7 @@
"creationDate": "Erstellt",
"lastModified": "Zuletzt bearbeitet",
"type": "Typ",
"external": "Extern",
"members": "Mitglieder"
},
"groups": {
@@ -49,13 +50,15 @@
},
"groupForm": {
"subtitle": "Gruppe bearbeiten",
"externalSubtitle": "Externe Gruppe bearbeiten",
"submit": "Speichern",
"nameError": "Name ist ungültig",
"descriptionError": "Beschreibung ist ungültig",
"help": {
"nameHelpText": "Eindeutiger Name der Gruppe",
"descriptionHelpText": "Eine kurze Beschreibung der Gruppe",
"memberHelpText": "Benutzername des Mitglieds der Gruppe"
"memberHelpText": "Benutzername des Mitglieds der Gruppe",
"externalHelpText": "Mitglieder dieser Gruppe werden von einem externen System wie z.B.: einem LDAP-Server verwaltet"
}
},
"deleteGroup": {

View File

@@ -4,7 +4,6 @@
"displayName": "Anzeigename",
"mail": "E-Mail",
"password": "Passwort",
"admin": "Admin",
"active": "Aktiv",
"type": "Typ",
"creationDate": "Erstellt",

View File

@@ -30,19 +30,6 @@
"base-url": "Base URL",
"force-base-url": "Force Base URL"
},
"admin-settings": {
"name": "Administration Settings",
"admin-groups": "Admin Groups",
"admin-users": "Admin Users",
"remove-group-button": "Remove Admin Group",
"remove-user-button": "Remove Admin User",
"add-group-error": "The group name you want to add is not valid",
"add-group-textfield": "Add group you want to add to admin groups here",
"add-group-button": "Add Admin Group",
"add-user-error": "The user name you want to add is not valid",
"add-user-textfield": "Add user you want to add to admin users here",
"add-user-button": "Add Admin User"
},
"login-attempt": {
"name": "Login Attempt",
"login-attempt-limit": "Login Attempt Limit",

View File

@@ -5,6 +5,7 @@
"creationDate": "Creation Date",
"lastModified": "Last Modified",
"type": "Type",
"external": "External",
"members": "Members"
},
"groups": {
@@ -49,13 +50,15 @@
},
"groupForm": {
"subtitle": "Edit Group",
"externalSubtitle": "Edit external group",
"submit": "Submit",
"nameError": "Group name is invalid",
"descriptionError": "Description is invalid",
"help": {
"nameHelpText": "Unique name of the group",
"descriptionHelpText": "A short description of the group",
"memberHelpText": "Usernames of the group members"
"memberHelpText": "Usernames of the group members",
"externalHelpText": "Members are managed by an external system such as LDAP"
}
},
"deleteGroup": {

View File

@@ -4,7 +4,6 @@
"displayName": "Display Name",
"mail": "E-Mail",
"password": "Password",
"admin": "Admin",
"active": "Active",
"type": "Type",
"creationDate": "Creation Date",

View File

@@ -1,93 +0,0 @@
// @flow
import React from "react";
import { translate } from "react-i18next";
import { Subtitle, AddEntryToTableField } from "@scm-manager/ui-components";
import AdminGroupTable from "../table/AdminGroupTable";
import AdminUserTable from "../table/AdminUserTable";
type Props = {
adminGroups: string[],
adminUsers: string[],
t: string => string,
onChange: (boolean, any, string) => void,
hasUpdatePermission: boolean
};
class AdminSettings extends React.Component<Props> {
render() {
const { t, adminGroups, adminUsers, hasUpdatePermission } = this.props;
return (
<div>
<Subtitle subtitle={t("admin-settings.name")} />
<div className="columns">
<div className="column is-half">
<AdminGroupTable
adminGroups={adminGroups}
onChange={(isValid, changedValue, name) =>
this.props.onChange(isValid, changedValue, name)
}
disabled={!hasUpdatePermission}
/>
<AddEntryToTableField
addEntry={this.addGroup}
disabled={!hasUpdatePermission}
buttonLabel={t("admin-settings.add-group-button")}
fieldLabel={t("admin-settings.add-group-textfield")}
errorMessage={t("admin-settings.add-group-error")}
/>
</div>
<div className="column is-half">
<AdminUserTable
adminUsers={adminUsers}
onChange={(isValid, changedValue, name) =>
this.props.onChange(isValid, changedValue, name)
}
disabled={!hasUpdatePermission}
/>
<AddEntryToTableField
addEntry={this.addUser}
disabled={!hasUpdatePermission}
buttonLabel={t("admin-settings.add-user-button")}
fieldLabel={t("admin-settings.add-user-textfield")}
errorMessage={t("admin-settings.add-user-error")}
/>
</div>
</div>
</div>
);
}
addGroup = (groupname: string) => {
if (this.isAdminGroupMember(groupname)) {
return;
}
this.props.onChange(
true,
[...this.props.adminGroups, groupname],
"adminGroups"
);
};
isAdminGroupMember = (groupname: string) => {
return this.props.adminGroups.includes(groupname);
};
addUser = (username: string) => {
if (this.isAdminUserMember(username)) {
return;
}
this.props.onChange(
true,
[...this.props.adminUsers, username],
"adminUsers"
);
};
isAdminUserMember = (username: string) => {
return this.props.adminUsers.includes(username);
};
}
export default translate("config")(AdminSettings);

View File

@@ -6,7 +6,6 @@ import type { NamespaceStrategies, Config } from "@scm-manager/ui-types";
import ProxySettings from "./ProxySettings";
import GeneralSettings from "./GeneralSettings";
import BaseUrlSettings from "./BaseUrlSettings";
import AdminSettings from "./AdminSettings";
import LoginAttempt from "./LoginAttempt";
type Props = {
@@ -46,8 +45,6 @@ class ConfigForm extends React.Component<Props, State> {
disableGroupingGrid: false,
dateFormat: "",
anonymousAccessEnabled: false,
adminGroups: [],
adminUsers: [],
baseUrl: "",
forceBaseUrl: false,
loginAttemptLimit: 0,
@@ -155,15 +152,6 @@ class ConfigForm extends React.Component<Props, State> {
hasUpdatePermission={configUpdatePermission}
/>
<hr />
<AdminSettings
adminGroups={config.adminGroups}
adminUsers={config.adminUsers}
onChange={(isValid, changedValue, name) =>
this.onChange(isValid, changedValue, name)
}
hasUpdatePermission={configUpdatePermission}
/>
<hr />
<ProxySettings
proxyPassword={config.proxyPassword ? config.proxyPassword : ""}
proxyPort={config.proxyPort}

View File

@@ -1,37 +0,0 @@
//@flow
import React from "react";
import { translate } from "react-i18next";
import ArrayConfigTable from "./ArrayConfigTable";
type Props = {
adminGroups: string[],
onChange: (boolean, any, string) => void,
disabled: boolean,
// context props
t: string => string
};
type State = {};
class AdminGroupTable extends React.Component<Props, State> {
render() {
const { t, disabled, adminGroups } = this.props;
return (
<ArrayConfigTable
items={adminGroups}
label={t("admin-settings.admin-groups")}
removeLabel={t("admin-settings.remove-group-button")}
onRemove={this.removeEntry}
disabled={disabled}
helpText={t("help.adminGroupsHelpText")}
/>
);
}
removeEntry = (newGroups: string[]) => {
this.props.onChange(true, newGroups, "adminGroups");
};
}
export default translate("config")(AdminGroupTable);

View File

@@ -1,35 +0,0 @@
//@flow
import React from "react";
import { translate } from "react-i18next";
import ArrayConfigTable from "./ArrayConfigTable";
type Props = {
adminUsers: string[],
onChange: (boolean, any, string) => void,
disabled: boolean,
// context props
t: string => string
};
class AdminUserTable extends React.Component<Props> {
render() {
const { adminUsers, t, disabled } = this.props;
return (
<ArrayConfigTable
items={adminUsers}
label={t("admin-settings.admin-users")}
removeLabel={t("admin-settings.remove-user-button")}
onRemove={this.removeEntry}
disabled={disabled}
helpText={t("help.adminUsersHelpText")}
/>
);
}
removeEntry = (newUsers: string[]) => {
this.props.onChange(true, newUsers, "adminUsers");
};
}
export default translate("config")(AdminUserTable);

View File

@@ -8,7 +8,8 @@ import {
MemberNameTable,
InputField,
SubmitButton,
Textarea
Textarea,
Checkbox
} from "@scm-manager/ui-components";
import type { Group, SelectValue } from "@scm-manager/ui-types";
@@ -67,16 +68,68 @@ class GroupForm extends React.Component<Props, State> {
submit = (event: Event) => {
event.preventDefault();
if (this.isValid()) {
this.props.submitForm(this.state.group);
const { group } = this.state;
if (group.external) {
group.members = [];
}
this.props.submitForm(group);
}
};
renderMemberfields = (group: Group) => {
if (group.external) {
return null;
}
const { loadUserSuggestions, t } = this.props;
return (
<>
<LabelWithHelpIcon
label={t("group.members")}
helpText={t("groupForm.help.memberHelpText")}
/>
<MemberNameTable
members={group.members}
memberListChanged={this.memberListChanged}
/>
<AutocompleteAddEntryToTableField
addEntry={this.addMember}
disabled={false}
buttonLabel={t("add-member-button.label")}
fieldLabel={t("add-member-textfield.label")}
errorMessage={t("add-member-textfield.error")}
loadSuggestions={loadUserSuggestions}
placeholder={t("add-member-autocomplete.placeholder")}
loadingMessage={t("add-member-autocomplete.loading")}
noOptionsMessage={t("add-member-autocomplete.no-options")}
/>
</>
);
};
renderExternalField = (group: Group) => {
const { t } = this.props;
if (this.isExistingGroup()) {
return null;
}
return (
<Checkbox
label={t("group.external")}
checked={group.external}
helpText={t("groupForm.help.externalHelpText")}
onChange={this.handleExternalChange}
/>
);
};
isExistingGroup = () => !! this.props.group;
render() {
const { loading, t } = this.props;
const { group } = this.state;
let nameField = null;
let subtitle = null;
if (!this.props.group) {
if (!this.isExistingGroup()) {
// create new group
nameField = (
<InputField
@@ -88,8 +141,9 @@ class GroupForm extends React.Component<Props, State> {
helpText={t("groupForm.help.nameHelpText")}
/>
);
} else if (group.external) {
subtitle = <Subtitle subtitle={t("groupForm.externalSubtitle")} />;
} else {
// edit existing group
subtitle = <Subtitle subtitle={t("groupForm.subtitle")} />;
}
@@ -106,26 +160,8 @@ class GroupForm extends React.Component<Props, State> {
validationError={false}
helpText={t("groupForm.help.descriptionHelpText")}
/>
<LabelWithHelpIcon
label={t("group.members")}
helpText={t("groupForm.help.memberHelpText")}
/>
<MemberNameTable
members={group.members}
memberListChanged={this.memberListChanged}
/>
<AutocompleteAddEntryToTableField
addEntry={this.addMember}
disabled={false}
buttonLabel={t("add-member-button.label")}
fieldLabel={t("add-member-textfield.label")}
errorMessage={t("add-member-textfield.error")}
loadSuggestions={this.props.loadUserSuggestions}
placeholder={t("add-member-autocomplete.placeholder")}
loadingMessage={t("add-member-autocomplete.loading")}
noOptionsMessage={t("add-member-autocomplete.no-options")}
/>
{this.renderExternalField(group)}
{this.renderMemberfields(group)}
<SubmitButton
disabled={!this.isValid()}
label={t("groupForm.submit")}
@@ -176,6 +212,12 @@ class GroupForm extends React.Component<Props, State> {
group: { ...this.state.group, description }
});
};
handleExternalChange = (external: boolean) => {
this.setState({
group: { ...this.state.group, external }
});
};
}
export default translate("groups")(GroupForm);

View File

@@ -2,7 +2,7 @@
import React from "react";
import type { Group } from "@scm-manager/ui-types";
import GroupMember from "./GroupMember";
import { DateFromNow } from "@scm-manager/ui-components";
import { DateFromNow, Checkbox } from "@scm-manager/ui-components";
import { translate } from "react-i18next";
import injectSheet from "react-jss";
@@ -34,6 +34,12 @@ class Details extends React.Component<Props> {
<th>{t("group.description")}</th>
<td>{group.description}</td>
</tr>
<tr>
<th>{t("group.external")}</th>
<td>
<Checkbox checked={group.external} />
</td>
</tr>
<tr>
<th>{t("group.type")}</th>
<td>{group.type}</td>

View File

@@ -1,25 +1,29 @@
// @flow
import React from "react";
import { Link } from "react-router-dom";
import type { Group } from "@scm-manager/ui-types";
type Props = {
group: Group
};
export default class GroupRow extends React.Component<Props> {
renderLink(to: string, label: string) {
return <Link to={to}>{label}</Link>;
}
render() {
const { group } = this.props;
const to = `/group/${group.name}`;
return (
<tr>
<td>{this.renderLink(to, group.name)}</td>
<td className="is-hidden-mobile">{group.description}</td>
</tr>
);
}
}
// @flow
import React from "react";
import { Link } from "react-router-dom";
import type { Group } from "@scm-manager/ui-types";
import { Checkbox } from "@scm-manager/ui-components"
type Props = {
group: Group
};
export default class GroupRow extends React.Component<Props> {
renderLink(to: string, label: string) {
return <Link to={to}>{label}</Link>;
}
render() {
const { group } = this.props;
const to = `/group/${group.name}`;
return (
<tr>
<td>{this.renderLink(to, group.name)}</td>
<td className="is-hidden-mobile">{group.description}</td>
<td>
<Checkbox checked={group.external} />
</td>
</tr>
);
}
}

View File

@@ -1,33 +1,34 @@
// @flow
import React from "react";
import { translate } from "react-i18next";
import GroupRow from "./GroupRow";
import type { Group } from "@scm-manager/ui-types";
type Props = {
t: string => string,
groups: Group[]
};
class GroupTable extends React.Component<Props> {
render() {
const { groups, t } = this.props;
return (
<table className="card-table table is-hoverable is-fullwidth">
<thead>
<tr>
<th>{t("group.name")}</th>
<th className="is-hidden-mobile">{t("group.description")}</th>
</tr>
</thead>
<tbody>
{groups.map((group, index) => {
return <GroupRow key={index} group={group} />;
})}
</tbody>
</table>
);
}
}
export default translate("groups")(GroupTable);
// @flow
import React from "react";
import { translate } from "react-i18next";
import GroupRow from "./GroupRow";
import type { Group } from "@scm-manager/ui-types";
type Props = {
t: string => string,
groups: Group[]
};
class GroupTable extends React.Component<Props> {
render() {
const { groups, t } = this.props;
return (
<table className="card-table table is-hoverable is-fullwidth">
<thead>
<tr>
<th>{t("group.name")}</th>
<th className="is-hidden-mobile">{t("group.description")}</th>
<th>{t("group.external")}</th>
</tr>
</thead>
<tbody>
{groups.map((group, index) => {
return <GroupRow key={index} group={group} />;
})}
</tbody>
</table>
);
}
}
export default translate("groups")(GroupTable);

View File

@@ -37,7 +37,6 @@ class UserForm extends React.Component<Props, State> {
displayName: "",
mail: "",
password: "",
admin: false,
active: true,
_links: {}
},
@@ -167,12 +166,6 @@ class UserForm extends React.Component<Props, State> {
<div className="columns">
<div className="column">
{passwordChangeField}
<Checkbox
label={t("user.admin")}
onChange={this.handleAdminChange}
checked={user ? user.admin : false}
helpText={t("help.adminHelpText")}
/>
<Checkbox
label={t("user.active")}
onChange={this.handleActiveChange}
@@ -225,10 +218,6 @@ class UserForm extends React.Component<Props, State> {
});
};
handleAdminChange = (admin: boolean) => {
this.setState({ user: { ...this.state.user, admin } });
};
handleActiveChange = (active: boolean) => {
this.setState({ user: { ...this.state.user, active } });
};

View File

@@ -1,66 +1,60 @@
//@flow
import React from "react";
import type { User } from "@scm-manager/ui-types";
import { translate } from "react-i18next";
import { Checkbox, MailLink, DateFromNow } from "@scm-manager/ui-components";
type Props = {
user: User,
t: string => string
};
class Details extends React.Component<Props> {
render() {
const { user, t } = this.props;
return (
<table className="table">
<tbody>
<tr>
<td className="has-text-weight-semibold">{t("user.name")}</td>
<td>{user.name}</td>
</tr>
<tr>
<td className="has-text-weight-semibold">{t("user.displayName")}</td>
<td>{user.displayName}</td>
</tr>
<tr>
<td className="has-text-weight-semibold">{t("user.mail")}</td>
<td>
<MailLink address={user.mail} />
</td>
</tr>
<tr>
<td className="has-text-weight-semibold">{t("user.admin")}</td>
<td>
<Checkbox checked={user.admin} />
</td>
</tr>
<tr>
<td className="has-text-weight-semibold">{t("user.active")}</td>
<td>
<Checkbox checked={user.active} />
</td>
</tr>
<tr>
<td className="has-text-weight-semibold">{t("user.type")}</td>
<td>{user.type}</td>
</tr>
<tr>
<td className="has-text-weight-semibold">{t("user.creationDate")}</td>
<td>
<DateFromNow date={user.creationDate} />
</td>
</tr>
<tr>
<td className="has-text-weight-semibold">{t("user.lastModified")}</td>
<td>
<DateFromNow date={user.lastModified} />
</td>
</tr>
</tbody>
</table>
);
}
}
export default translate("users")(Details);
//@flow
import React from "react";
import type { User } from "@scm-manager/ui-types";
import { translate } from "react-i18next";
import { Checkbox, MailLink, DateFromNow } from "@scm-manager/ui-components";
type Props = {
user: User,
t: string => string
};
class Details extends React.Component<Props> {
render() {
const { user, t } = this.props;
return (
<table className="table">
<tbody>
<tr>
<td className="has-text-weight-semibold">{t("user.name")}</td>
<td>{user.name}</td>
</tr>
<tr>
<td className="has-text-weight-semibold">{t("user.displayName")}</td>
<td>{user.displayName}</td>
</tr>
<tr>
<td className="has-text-weight-semibold">{t("user.mail")}</td>
<td>
<MailLink address={user.mail} />
</td>
</tr>
<tr>
<td className="has-text-weight-semibold">{t("user.active")}</td>
<td>
<Checkbox checked={user.active} />
</td>
</tr>
<tr>
<td className="has-text-weight-semibold">{t("user.type")}</td>
<td>{user.type}</td>
</tr>
<tr>
<td className="has-text-weight-semibold">{t("user.creationDate")}</td>
<td>
<DateFromNow date={user.creationDate} />
</td>
</tr>
<tr>
<td className="has-text-weight-semibold">{t("user.lastModified")}</td>
<td>
<DateFromNow date={user.lastModified} />
</td>
</tr>
</tbody>
</table>
);
}
}
export default translate("users")(Details);

View File

@@ -1,31 +1,31 @@
// @flow
import React from "react";
import { Link } from "react-router-dom";
import type { User } from "@scm-manager/ui-types";
type Props = {
user: User
};
export default class UserRow extends React.Component<Props> {
renderLink(to: string, label: string) {
return <Link to={to}>{label}</Link>;
}
render() {
const { user } = this.props;
const to = `/user/${user.name}`;
return (
<tr>
<td className="is-hidden-mobile">{this.renderLink(to, user.name)}</td>
<td>{this.renderLink(to, user.displayName)}</td>
<td>
<a href={`mailto: ${user.mail}`}>{user.mail}</a>
</td>
<td className="is-hidden-mobile">
<input type="checkbox" id="admin" checked={user.admin} readOnly />
</td>
</tr>
);
}
}
// @flow
import React from "react";
import { Link } from "react-router-dom";
import type { User } from "@scm-manager/ui-types";
type Props = {
user: User
};
export default class UserRow extends React.Component<Props> {
renderLink(to: string, label: string) {
return <Link to={to}>{label}</Link>;
}
render() {
const { user } = this.props;
const to = `/user/${user.name}`;
return (
<tr>
<td className="is-hidden-mobile">{this.renderLink(to, user.name)}</td>
<td>{this.renderLink(to, user.displayName)}</td>
<td>
<a href={`mailto: ${user.mail}`}>{user.mail}</a>
</td>
<td className="is-hidden-mobile">
<input type="checkbox" id="active" checked={user.active} readOnly />
</td>
</tr>
);
}
}

View File

@@ -1,37 +1,37 @@
// @flow
import React from "react";
import { translate } from "react-i18next";
import UserRow from "./UserRow";
import type { User } from "@scm-manager/ui-types";
type Props = {
t: string => string,
users: User[]
};
;
class UserTable extends React.Component<Props> {
render() {
const { users, t } = this.props;
return (
<table className="card-table table is-hoverable is-fullwidth">
<thead>
<tr>
<th className="is-hidden-mobile">{t("user.name")}</th>
<th>{t("user.displayName")}</th>
<th>{t("user.mail")}</th>
<th className="is-hidden-mobile">{t("user.admin")}</th>
</tr>
</thead>
<tbody>
{users.map((user, index) => {
return <UserRow key={index} user={user} />;
})}
</tbody>
</table>
);
}
}
export default translate("users")(UserTable);
// @flow
import React from "react";
import { translate } from "react-i18next";
import UserRow from "./UserRow";
import type { User } from "@scm-manager/ui-types";
type Props = {
t: string => string,
users: User[]
};
;
class UserTable extends React.Component<Props> {
render() {
const { users, t } = this.props;
return (
<table className="card-table table is-hoverable is-fullwidth">
<thead>
<tr>
<th className="is-hidden-mobile">{t("user.name")}</th>
<th>{t("user.displayName")}</th>
<th>{t("user.mail")}</th>
<th className="is-hidden-mobile">{t("user.active")}</th>
</tr>
</thead>
<tbody>
{users.map((user, index) => {
return <UserRow key={index} user={user} />;
})}
</tbody>
</table>
);
}
}
export default translate("users")(UserTable);

View File

@@ -1,463 +0,0 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.api.rest.resources;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.inject.Inject;
import com.google.inject.Singleton;
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.AuthenticationException;
import org.apache.shiro.authc.DisabledAccountException;
import org.apache.shiro.authc.ExcessiveAttemptsException;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.ScmState;
import sonia.scm.ScmStateFactory;
import sonia.scm.api.rest.RestActionResult;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.security.Tokens;
import sonia.scm.util.HttpUtil;
//~--- JDK imports ------------------------------------------------------------
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import sonia.scm.security.AccessToken;
import sonia.scm.security.AccessTokenBuilder;
import sonia.scm.security.AccessTokenBuilderFactory;
import sonia.scm.security.AccessTokenCookieIssuer;
import sonia.scm.security.Scope;
/**
* Authentication related RESTful Web Service endpoint.
*
* @author Sebastian Sdorra
*/
@Singleton
@Path("auth")
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public class AuthenticationResource
{
/** the logger for AuthenticationResource */
private static final Logger logger =
LoggerFactory.getLogger(AuthenticationResource.class);
//~--- constant enums -------------------------------------------------------
/**
* Enum description
*
*/
private static enum WUIAuthenticationFailure { LOCKED, TEMPORARY_LOCKED,
WRONG_CREDENTIALS; }
//~--- constructors ---------------------------------------------------------
/**
* Constructs ...
*
*
* @param configuration
* @param stateFactory
* @param tokenBuilderFactory
* @param cookieIssuer
*/
@Inject
public AuthenticationResource(ScmConfiguration configuration,
ScmStateFactory stateFactory, AccessTokenBuilderFactory tokenBuilderFactory, AccessTokenCookieIssuer cookieIssuer)
{
this.configuration = configuration;
this.stateFactory = stateFactory;
this.tokenBuilderFactory = tokenBuilderFactory;
this.cookieIssuer = cookieIssuer;
}
//~--- methods --------------------------------------------------------------
/**
* Authenticate a user and return the state of the application.
*
* @param request current http request
* @param response current http response
* @param grantType grant type, currently only password is supported
* @param username the username for the authentication
* @param password the password for the authentication
* @param cookie create authentication token
* @param scope scope of created token
*
* @return
*/
@POST
@Path("access_token")
@TypeHint(ScmState.class)
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 400, condition = "bad request, required parameter is missing"),
@ResponseCode(code = 401, condition = "unauthorized, the specified username or password is wrong"),
@ResponseCode(code = 500, condition = "internal server error")
})
public Response authenticate(
@Context HttpServletRequest request,
@Context HttpServletResponse response,
@FormParam("grant_type") String grantType,
@FormParam("username") String username,
@FormParam("password") String password,
@FormParam("cookie") boolean cookie,
@FormParam("scope") List<String> scope)
{
Preconditions.checkArgument(!Strings.isNullOrEmpty(grantType), "grant_type parameter is required");
Preconditions.checkArgument(!Strings.isNullOrEmpty(username), "username parameter is required");
Preconditions.checkArgument(!Strings.isNullOrEmpty(password), "password parameter is required");
Response res;
Subject subject = SecurityUtils.getSubject();
try
{
subject.login(Tokens.createAuthenticationToken(request, username, password));
AccessTokenBuilder tokenBuilder = tokenBuilderFactory.create();
if ( scope != null ) {
tokenBuilder.scope(Scope.valueOf(scope));
}
AccessToken token = tokenBuilder.build();
ScmState state;
if (cookie) {
cookieIssuer.authenticate(request, response, token);
state = stateFactory.createState(subject);
} else {
state = stateFactory.createState(subject, token.compact());
}
res = Response.ok(state).build();
}
catch (DisabledAccountException ex)
{
if (logger.isTraceEnabled())
{
logger.trace(
"authentication failed, account user ".concat(username).concat(
" is locked"), ex);
}
else
{
logger.warn("authentication failed, account {} is locked", username);
}
res = handleFailedAuthentication(request, ex, Response.Status.FORBIDDEN,
WUIAuthenticationFailure.LOCKED);
}
catch (ExcessiveAttemptsException ex)
{
if (logger.isTraceEnabled())
{
logger.trace(
"authentication failed, account user ".concat(username).concat(
" is temporary locked"), ex);
}
else
{
logger.warn("authentication failed, account {} is temporary locked", username);
}
res = handleFailedAuthentication(request, ex, Response.Status.FORBIDDEN,
WUIAuthenticationFailure.TEMPORARY_LOCKED);
}
catch (AuthenticationException ex)
{
if (logger.isTraceEnabled())
{
logger.trace("authentication failed for user ".concat(username), ex);
}
else
{
logger.warn("authentication failed for user {}", username);
}
res = handleFailedAuthentication(request, ex, Response.Status.UNAUTHORIZED,
WUIAuthenticationFailure.WRONG_CREDENTIALS);
}
return res;
}
/**
* Logout the current user. Returns the current state of the application, if public access is enabled.
*
* @param request the current http request
* @param response the current http response
*
* @return
*/
@GET
@Path("logout")
@TypeHint(ScmState.class)
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 500, condition = "internal server error")
})
public Response logout(@Context HttpServletRequest request, @Context HttpServletResponse response)
{
Subject subject = SecurityUtils.getSubject();
subject.logout();
// remove authentication cookie
cookieIssuer.invalidate(request, response);
Response resp;
if (configuration.isAnonymousAccessEnabled())
{
resp = Response.ok(stateFactory.createAnonymousState()).build();
}
else
{
resp = Response.ok().build();
}
return resp;
}
//~--- get methods ----------------------------------------------------------
/**
* This method is an alias of the {@link #getState(HttpServletRequest)} method.
* The only difference between the methods, is that this one could not be used with basic authentication.
*
* @param request the current http request
*
* @return
*/
@GET
@Path("state")
@TypeHint(ScmState.class)
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 401, condition = "unauthorized, user is not authenticated and public access is disabled"),
@ResponseCode(code = 500, condition = "internal server error")
})
public Response getCurrentState(@Context HttpServletRequest request)
{
return getState(request);
}
/**
* Returns the current state of the application.
*
* @param request the current http request
*
* @return
*/
@GET
@TypeHint(ScmState.class)
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 401, condition = "unauthorized, user is not authenticated and public access is disabled"),
@ResponseCode(code = 500, condition = "internal server error")
})
public Response getState(@Context HttpServletRequest request)
{
Response response;
Subject subject = SecurityUtils.getSubject();
if (subject.isAuthenticated() || subject.isRemembered())
{
if (logger.isDebugEnabled())
{
String auth = subject.isRemembered()
? "remembered"
: "authenticated";
logger.debug("return state for {} user {}", auth,
subject.getPrincipal());
}
ScmState state = stateFactory.createState(subject);
response = Response.ok(state).build();
}
else if (configuration.isAnonymousAccessEnabled())
{
response = Response.ok(stateFactory.createAnonymousState()).build();
}
else
{
response = Response.status(Response.Status.UNAUTHORIZED).build();
}
return response;
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param request
* @param ex
* @param status
* @param failure
*
* @return
*/
private Response handleFailedAuthentication(HttpServletRequest request,
AuthenticationException ex, Response.Status status,
WUIAuthenticationFailure failure)
{
Response response;
if (HttpUtil.isWUIRequest(request))
{
response = Response.ok(new WUIAuthenticationFailedResult(failure,
ex.getMessage())).build();
}
else
{
response = Response.status(status).build();
}
return response;
}
//~--- inner classes --------------------------------------------------------
/**
* Class description
*
*
* @version Enter version here..., 13/09/28
* @author Enter your name here...
*/
@XmlRootElement(name = "result")
@XmlAccessorType(XmlAccessType.FIELD)
private static final class WUIAuthenticationFailedResult
extends RestActionResult
{
/**
* Constructs ...
*
*
* @param failure
* @param mesage
*/
public WUIAuthenticationFailedResult(WUIAuthenticationFailure failure,
String mesage)
{
super(false);
this.failure = failure;
this.mesage = mesage;
}
//~--- get methods --------------------------------------------------------
/**
* Method description
*
*
* @return
*/
public WUIAuthenticationFailure getFailure()
{
return failure;
}
/**
* Method description
*
*
* @return
*/
public String getMesage()
{
return mesage;
}
//~--- fields -------------------------------------------------------------
/** Field description */
private final WUIAuthenticationFailure failure;
/** Field description */
private final String mesage;
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private final ScmConfiguration configuration;
/** Field description */
private final ScmStateFactory stateFactory;
/** Field description */
private final AccessTokenBuilderFactory tokenBuilderFactory;
/** Field description */
private final AccessTokenCookieIssuer cookieIssuer;
}

View File

@@ -1,87 +0,0 @@
/**
* Copyright (c) 2010, Sebastian Sdorra All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer. 2. Redistributions in
* binary form must reproduce the above copyright notice, this list of
* conditions and the following disclaimer in the documentation and/or other
* materials provided with the distribution. 3. Neither the name of SCM-Manager;
* nor the names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.api.rest.resources;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import org.apache.shiro.SecurityUtils;
import sonia.scm.security.CipherUtil;
import sonia.scm.security.Role;
//~--- JDK imports ------------------------------------------------------------
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
/**
* Rest resource to encrypt values.
*
* @author Sebastian Sdorra
* @since 1.41
*/
@Path("security/cipher")
public class CipherResource
{
/**
* Encrypts the request body and returns an encrypted string. This method can
* only executed with administration privileges.
*
* @param value value to encrypt
*
* @return unique key
*/
@POST
@Path("encrypt")
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Produces(MediaType.TEXT_PLAIN)
public String encrypt(String value)
{
SecurityUtils.getSubject().checkRole(Role.ADMIN);
Preconditions.checkArgument(!Strings.isNullOrEmpty(value),
"value is required");
return CipherUtil.getInstance().encode(value);
}
}

View File

@@ -1,98 +0,0 @@
/**
* Copyright (c) 2010, Sebastian Sdorra All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer. 2. Redistributions in
* binary form must reproduce the above copyright notice, this list of
* conditions and the following disclaimer in the documentation and/or other
* materials provided with the distribution. 3. Neither the name of SCM-Manager;
* nor the names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.api.rest.resources;
//~--- non-JDK imports --------------------------------------------------------
import com.google.inject.Inject;
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import org.apache.shiro.SecurityUtils;
import sonia.scm.security.KeyGenerator;
import sonia.scm.security.Role;
//~--- JDK imports ------------------------------------------------------------
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
/**
* Rest resource to generate unique keys.
*
* @author Sebastian Sdorra
* @since 1.41
*/
@Path("security/key")
public class KeyResource
{
/**
* Constructs a new KeyResource.
*
*
* @param keyGenerator key generator
*/
@Inject
public KeyResource(KeyGenerator keyGenerator)
{
this.keyGenerator = keyGenerator;
}
//~--- methods --------------------------------------------------------------
/**
* Generates a unique key. <strong>Note:</strong> This method can only executed with administration privileges.
*
* @return unique key
*/
@GET
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Produces(MediaType.TEXT_PLAIN)
public String generateKey()
{
SecurityUtils.getSubject().checkRole(Role.ADMIN);
return keyGenerator.createKey();
}
//~--- fields ---------------------------------------------------------------
/** key generator */
private final KeyGenerator keyGenerator;
}

View File

@@ -42,24 +42,43 @@ import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.ResponseHeader;
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint;
import org.apache.shiro.SecurityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.NotFoundException;
import sonia.scm.FeatureNotSupportedException;
import sonia.scm.NotFoundException;
import sonia.scm.Type;
import sonia.scm.api.rest.RestActionUploadResult;
import sonia.scm.api.v2.resources.RepositoryResource;
import sonia.scm.repository.*;
import sonia.scm.repository.AdvancedImportHandler;
import sonia.scm.repository.ImportHandler;
import sonia.scm.repository.ImportResult;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryHandler;
import sonia.scm.repository.RepositoryManager;
import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.repository.RepositoryType;
import sonia.scm.repository.api.Command;
import sonia.scm.repository.api.RepositoryService;
import sonia.scm.repository.api.RepositoryServiceFactory;
import sonia.scm.repository.api.UnbundleCommandBuilder;
import sonia.scm.security.Role;
import sonia.scm.util.IOUtil;
import javax.ws.rs.*;
import javax.ws.rs.core.*;
import javax.ws.rs.Consumes;
import javax.ws.rs.DefaultValue;
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.GenericEntity;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
@@ -233,7 +252,7 @@ public class RepositoryImportResource
public Response importFromUrl(@Context UriInfo uriInfo,
@PathParam("type") String type, UrlImportRequest request)
{
SecurityUtils.getSubject().checkRole(Role.ADMIN);
RepositoryPermissions.create().check();
checkNotNull(request, "request is required");
checkArgument(!Strings.isNullOrEmpty(request.getName()),
"request does not contain name of the repository");
@@ -288,7 +307,7 @@ public class RepositoryImportResource
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public Response importRepositories(@PathParam("type") String type)
{
SecurityUtils.getSubject().checkRole(Role.ADMIN);
RepositoryPermissions.create().check();
List<Repository> repositories = new ArrayList<Repository>();
@@ -320,7 +339,7 @@ public class RepositoryImportResource
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public Response importRepositories()
{
SecurityUtils.getSubject().checkRole(Role.ADMIN);
RepositoryPermissions.create().check();
logger.info("start directory import for all supported repository types");
@@ -363,7 +382,7 @@ public class RepositoryImportResource
public Response importRepositoriesFromDirectory(
@PathParam("type") String type)
{
SecurityUtils.getSubject().checkRole(Role.ADMIN);
RepositoryPermissions.create().check();
Response response;
@@ -438,7 +457,7 @@ public class RepositoryImportResource
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public Response getImportableTypes()
{
SecurityUtils.getSubject().checkRole(Role.ADMIN);
RepositoryPermissions.create().check();
List<Type> types = findImportableTypes();
@@ -537,7 +556,7 @@ public class RepositoryImportResource
private Repository doImportFromBundle(String type, String name,
InputStream inputStream, boolean compressed)
{
SecurityUtils.getSubject().checkRole(Role.ADMIN);
RepositoryPermissions.create().check();
checkArgument(!Strings.isNullOrEmpty(name),
"request does not contain name of the repository");

View File

@@ -1,463 +0,0 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.api.rest.resources;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.inject.Inject;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import sonia.scm.SCMContextProvider;
import sonia.scm.ServletContainerDetector;
import sonia.scm.Type;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.plugin.PluginManager;
import sonia.scm.repository.RepositoryHandler;
import sonia.scm.repository.RepositoryManager;
import sonia.scm.security.Role;
import sonia.scm.security.ScmSecurityException;
import sonia.scm.util.SystemUtil;
//~--- JDK imports ------------------------------------------------------------
import java.io.IOException;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import sonia.scm.store.ConfigurationStoreFactory;
import sonia.scm.template.Viewable;
/**
*
* @author Sebastian Sdorra
*/
@Path("support")
public class SupportResource
{
/** Field description */
public static final String TEMPLATE = "/templates/support.mustache";
//~--- constructors ---------------------------------------------------------
/**
* Constructs ...
*
*
*
* @param securityContext
* @param context
* @param templateHandler
* @param configuration
* @param pluginManager
* @param storeFactory
* @param repositoryManager
* @param request
*/
@Inject
public SupportResource(SCMContextProvider context,
ScmConfiguration configuration, PluginManager pluginManager,
ConfigurationStoreFactory storeFactory, RepositoryManager repositoryManager,
HttpServletRequest request)
{
this.context = context;
this.configuration = configuration;
this.pluginManager = pluginManager;
this.storeFactoryClass = storeFactory.getClass();
this.repositoryManager = repositoryManager;
this.request = request;
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @return
*
* @throws IOException
*/
@GET
@Produces(MediaType.TEXT_HTML)
public Viewable getSupport() throws IOException
{
Subject subject = SecurityUtils.getSubject();
if (!subject.hasRole(Role.ADMIN))
{
throw new ScmSecurityException("admin privileges required");
}
Map<String, Object> env = Maps.newHashMap();
env.put("version", new VersionInformation(context, storeFactoryClass));
env.put("configuration", configuration);
env.put("pluginManager", pluginManager);
env.put("runtime", new RuntimeInformation());
env.put("system", new SystemInformation(request));
env.put("repositoryHandlers", getRepositoryHandlers());
return new Viewable(TEMPLATE, env);
}
/**
* Method description
*
*
* @return
*/
private List<RepositoryHandler> getRepositoryHandlers()
{
List<RepositoryHandler> handlers = Lists.newArrayList();
for (Type type : repositoryManager.getConfiguredTypes())
{
handlers.add(repositoryManager.getHandler(type.getName()));
}
return handlers;
}
//~--- inner classes --------------------------------------------------------
/**
* Class description
*
*
* @version Enter version here..., 12/04/30
* @author Enter your name here...
*/
public static class RuntimeInformation
{
/**
* Constructs ...
*
*/
public RuntimeInformation()
{
Runtime runtime = Runtime.getRuntime();
totalMemory = runtime.totalMemory();
freeMemory = runtime.freeMemory();
maxMemory = runtime.maxMemory();
availableProcessors = runtime.availableProcessors();
}
//~--- get methods --------------------------------------------------------
/**
* Method description
*
*
* @return
*/
public int getAvailableProcessors()
{
return availableProcessors;
}
/**
* Method description
*
*
* @return
*/
public long getFreeMemory()
{
return freeMemory;
}
/**
* Method description
*
*
* @return
*/
public long getMaxMemory()
{
return maxMemory;
}
/**
* Method description
*
*
* @return
*/
public long getTotalMemory()
{
return totalMemory;
}
//~--- fields -------------------------------------------------------------
/** Field description */
private int availableProcessors;
/** Field description */
private long freeMemory;
/** Field description */
private long maxMemory;
/** Field description */
private long totalMemory;
}
/**
* Class description
*
*
* @version Enter version here..., 12/04/30
* @author Enter your name here...
*/
public static class SystemInformation
{
/**
* Constructs ...
*
*
* @param request
*/
public SystemInformation(HttpServletRequest request)
{
os = SystemUtil.getOS();
arch = SystemUtil.getArch();
container = ServletContainerDetector.detect(request).name();
java = System.getProperty("java.vendor").concat("/").concat(
System.getProperty("java.version"));
locale = Locale.getDefault().toString();
timeZone = TimeZone.getDefault().getID();
}
//~--- get methods --------------------------------------------------------
/**
* Method description
*
*
* @return
*/
public String getArch()
{
return arch;
}
/**
* Method description
*
*
* @return
*/
public String getContainer()
{
return container;
}
/**
* Method description
*
*
* @return
*/
public String getJava()
{
return java;
}
/**
* Method description
*
*
* @return
*/
public String getLocale()
{
return locale;
}
/**
* Method description
*
*
* @return
*/
public String getOs()
{
return os;
}
/**
* Method description
*
*
* @return
*/
public String getTimeZone()
{
return timeZone;
}
//~--- fields -------------------------------------------------------------
/** Field description */
private String arch;
/** Field description */
private String container;
/** Field description */
private String java;
/** Field description */
private String locale;
/** Field description */
private String os;
/** Field description */
private String timeZone;
}
/**
* Class description
*
*
* @version Enter version here..., 12/04/30
* @author Enter your name here...
*/
public static class VersionInformation
{
/**
* Constructs ...
*
*
* @param context
* @param storeFactoryClass
*/
public VersionInformation(SCMContextProvider context,
Class<?> storeFactoryClass)
{
version = context.getVersion();
stage = context.getStage().name();
storeFactory = storeFactoryClass.getName();
}
//~--- get methods --------------------------------------------------------
/**
* Method description
*
*
* @return
*/
public String getStage()
{
return stage;
}
/**
* Method description
*
*
* @return
*/
public String getStoreFactory()
{
return storeFactory;
}
/**
* Method description
*
*
* @return
*/
public String getVersion()
{
return version;
}
//~--- fields -------------------------------------------------------------
/** Field description */
private String stage;
/** Field description */
private String storeFactory;
/** Field description */
private String version;
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private ScmConfiguration configuration;
/** Field description */
private SCMContextProvider context;
/** Field description */
private PluginManager pluginManager;
/** Field description */
private RepositoryManager repositoryManager;
/** Field description */
private HttpServletRequest request;
/** Field description */
private Class<?> storeFactoryClass;
}

View File

@@ -23,10 +23,6 @@ public class ConfigDto extends HalRepresentation {
private boolean disableGroupingGrid;
private String dateFormat;
private boolean anonymousAccessEnabled;
@NoBlankStrings
private Set<String> adminGroups;
@NoBlankStrings
private Set<String> adminUsers;
private String baseUrl;
private boolean forceBaseUrl;
private int loginAttemptLimit;

View File

@@ -27,6 +27,7 @@ public class GroupDto extends HalRepresentation {
private String type;
private Map<String, String> properties;
private List<String> members;
private boolean external;
GroupDto(Links links, Embedded embedded) {
super(links, embedded);

View File

@@ -19,7 +19,6 @@ import static sonia.scm.api.v2.ValidationConstraints.USER_GROUP_PATTERN;
@NoArgsConstructor @Getter @Setter
public class UserDto extends HalRepresentation {
private boolean active;
private boolean admin;
private Instant creationDate;
@NotEmpty
private String displayName;

View File

@@ -0,0 +1,71 @@
package sonia.scm.boot;
import com.google.common.annotations.VisibleForTesting;
import org.apache.shiro.authc.credential.PasswordService;
import sonia.scm.plugin.Extension;
import sonia.scm.security.PermissionAssigner;
import sonia.scm.security.PermissionDescriptor;
import sonia.scm.user.User;
import sonia.scm.user.UserManager;
import sonia.scm.web.security.AdministrationContext;
import sonia.scm.web.security.PrivilegedAction;
import javax.inject.Inject;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import java.util.Collections;
@Extension
public class SetupContextListener implements ServletContextListener {
private final AdministrationContext administrationContext;
@Inject
public SetupContextListener(AdministrationContext administrationContext) {
this.administrationContext = administrationContext;
}
@Override
public void contextInitialized(ServletContextEvent sce) {
administrationContext.runAsAdmin(SetupAction.class);
}
@Override
public void contextDestroyed(ServletContextEvent sce) {}
@VisibleForTesting
static class SetupAction implements PrivilegedAction {
private final UserManager userManager;
private final PasswordService passwordService;
private final PermissionAssigner permissionAssigner;
@Inject
public SetupAction(UserManager userManager, PasswordService passwordService, PermissionAssigner permissionAssigner) {
this.userManager = userManager;
this.passwordService = passwordService;
this.permissionAssigner = permissionAssigner;
}
@Override
public void run() {
if (isFirstStart()) {
createAdminAccount();
}
}
private boolean isFirstStart() {
return userManager.getAll().isEmpty();
}
private void createAdminAccount() {
User scmadmin = new User("scmadmin", "SCM Administrator", "scm-admin@scm-manager.org");
String password = passwordService.encryptPassword("scmadmin");
scmadmin.setPassword(password);
userManager.create(scmadmin);
PermissionDescriptor descriptor = new PermissionDescriptor("*");
permissionAssigner.setPermissionsForUser("scmadmin", Collections.singleton(descriptor));
}
}
}

View File

@@ -36,6 +36,7 @@ import com.google.common.collect.Multimap;
import com.google.inject.Singleton;
import org.apache.shiro.SecurityUtils;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.security.Role;
import java.util.Collection;
@@ -63,7 +64,8 @@ public final class DebugService
* Returns the last received hook data for the given repository.
*/
public DebugHookData getLast(NamespaceAndName namespaceAndName){
SecurityUtils.getSubject().checkRole(Role.ADMIN);
// debug permission does not exists, so only accounts with "*" permission can use these resource
SecurityUtils.getSubject().checkPermission("debug");
DebugHookData hookData = null;
Collection<DebugHookData> receivedHookData = receivedHooks.get(namespaceAndName);
if (receivedHookData != null && ! receivedHookData.isEmpty()){
@@ -76,7 +78,8 @@ public final class DebugService
* Returns all received hook data for the given repository.
*/
public Collection<DebugHookData> getAll(NamespaceAndName namespaceAndName){
SecurityUtils.getSubject().checkRole(Role.ADMIN);
// debug permission does not exists, so only accounts with "*" permission can use these resource
SecurityUtils.getSubject().checkPermission("debug");
return receivedHooks.get(namespaceAndName);
}
}

View File

@@ -49,15 +49,15 @@ import sonia.scm.user.UserEvent;
import sonia.scm.user.UserModificationEvent;
/**
* Receives all kinds of events, which affects authorization relevant data and fires an
* Receives all kinds of events, which affects authorization relevant data and fires an
* {@link AuthorizationChangedEvent} if authorization data has changed.
*
*
* @author Sebastian Sdorra
* @since 1.52
*/
@EagerSingleton
public class AuthorizationChangedEventProducer {
/**
* the logger for AuthorizationChangedEventProducer
*/
@@ -68,7 +68,7 @@ public class AuthorizationChangedEventProducer {
*/
public AuthorizationChangedEventProducer() {
}
/**
* Invalidates the cache of a user which was modified. The cache entries for the user will be invalidated for the
* following reasons:
@@ -90,11 +90,11 @@ public class AuthorizationChangedEventProducer {
}
}
}
private boolean isModificationEvent(HandlerEvent<?> event) {
return event instanceof ModificationHandlerEvent;
}
private void handleUserEvent(UserEvent event) {
String username = event.getItem().getName();
logger.debug(
@@ -102,26 +102,26 @@ public class AuthorizationChangedEventProducer {
);
fireEventForUser(username);
}
private void handleUserModificationEvent(UserModificationEvent event) {
String username = event.getItem().getId();
User beforeModification = event.getItemBeforeModification();
if (isAuthorizationDataModified(event.getItem(), beforeModification)) {
logger.debug(
"fire authorization changed event for user {}, because of a authorization relevant field has changed",
"fire authorization changed event for user {}, because of a authorization relevant field has changed",
username
);
fireEventForUser(username);
} else {
logger.debug(
"authorization changed event for user {} is not fired, because no authorization relevant field has changed",
"authorization changed event for user {} is not fired, because no authorization relevant field has changed",
username
);
}
}
private boolean isAuthorizationDataModified(User user, User beforeModification) {
return user.isAdmin() != beforeModification.isAdmin() || user.isActive() != beforeModification.isActive();
return user.isActive() != beforeModification.isActive();
}
private void fireEventForUser(String username) {
@@ -148,7 +148,7 @@ public class AuthorizationChangedEventProducer {
}
}
}
private void handleRepositoryModificationEvent(RepositoryModificationEvent event) {
Repository repository = event.getItem();
if (isAuthorizationDataModified(repository, event.getItemBeforeModification())) {
@@ -169,14 +169,14 @@ public class AuthorizationChangedEventProducer {
|| repository.isPublicReadable() != beforeModification.isPublicReadable()
|| !(repository.getPermissions().containsAll(beforeModification.getPermissions()) && beforeModification.getPermissions().containsAll(repository.getPermissions()));
}
private void fireEventForEveryUser() {
sendEvent(AuthorizationChangedEvent.createForEveryUser());
}
private void handleRepositoryEvent(RepositoryEvent event){
logger.debug(
"fire authorization changed event, because of received {} event for repository {}",
"fire authorization changed event, because of received {} event for repository {}",
event.getEventType(), event.getItem().getName()
);
fireEventForEveryUser();
@@ -199,7 +199,7 @@ public class AuthorizationChangedEventProducer {
}
}
}
private void handleGroupPermissionChange(AssignedPermission permission) {
logger.debug(
"fire authorization changed event for group {}, because permission {} has changed",
@@ -207,13 +207,13 @@ public class AuthorizationChangedEventProducer {
);
fireEventForEveryUser();
}
private void handleUserPermissionChange(AssignedPermission permission) {
logger.debug(
"fire authorization changed event for user {}, because permission {} has changed",
permission.getName(), permission.getPermission()
);
fireEventForUser(permission.getName());
fireEventForUser(permission.getName());
}
/**
@@ -230,7 +230,7 @@ public class AuthorizationChangedEventProducer {
public void onEvent(GroupEvent event) {
if (event.getEventType().isPost()) {
if (isModificationEvent(event)) {
handleGroupModificationEvent((GroupModificationEvent) event);
handleGroupModificationEvent((GroupModificationEvent) event);
} else {
handleGroupEvent(event);
}
@@ -244,28 +244,28 @@ public class AuthorizationChangedEventProducer {
fireEventForEveryUser();
} else {
logger.debug(
"authorization changed event is not fired, because non relevant field of group {} has changed",
"authorization changed event is not fired, because non relevant field of group {} has changed",
group.getId()
);
}
}
private boolean isAuthorizationDataModified(Group group, Group beforeModification) {
return !group.getMembers().equals(beforeModification.getMembers());
}
private void handleGroupEvent(GroupEvent event){
logger.debug(
"fire authorization changed event, because of received group event {} for group {}",
event.getEventType(),
"fire authorization changed event, because of received group event {} for group {}",
event.getEventType(),
event.getItem().getId()
);
fireEventForEveryUser();
fireEventForEveryUser();
}
@VisibleForTesting
protected void sendEvent(AuthorizationChangedEvent event) {
ScmEventBus.getInstance().post(event);
}
}

View File

@@ -77,9 +77,6 @@ import java.util.Set;
public class DefaultAuthorizationCollector implements AuthorizationCollector
{
/** Field description */
private static final String ADMIN_PERMISSION = "*";
/** Field description */
private static final String CACHE_NAME = "sonia.cache.authorizing";
@@ -94,18 +91,14 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
/**
* Constructs ...
*
*
*
* @param configuration
* @param cacheManager
* @param repositoryDAO
* @param securitySystem
*/
@Inject
public DefaultAuthorizationCollector(ScmConfiguration configuration, CacheManager cacheManager,
public DefaultAuthorizationCollector(CacheManager cacheManager,
RepositoryDAO repositoryDAO, SecuritySystem securitySystem)
{
this.configuration = configuration;
this.cache = cacheManager.getCache(CACHE_NAME);
this.repositoryDAO = repositoryDAO;
this.securitySystem = securitySystem;
@@ -233,74 +226,22 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
}
}
private AuthorizationInfo createAuthorizationInfo(User user,
GroupNames groups)
{
Set<String> roles;
Set<String> permissions;
private AuthorizationInfo createAuthorizationInfo(User user, GroupNames groups) {
Builder<String> builder = ImmutableSet.builder();
if (isAdmin(user, groups))
{
if (logger.isDebugEnabled())
{
logger.debug("grant admin role for user {}", user.getName());
}
collectGlobalPermissions(builder, user, groups);
collectRepositoryPermissions(builder, user, groups);
builder.add(canReadOwnUser(user));
builder.add(getUserAutocompletePermission());
builder.add(getGroupAutocompletePermission());
builder.add(getChangeOwnPasswordPermission(user));
roles = ImmutableSet.of(Role.USER, Role.ADMIN);
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(ImmutableSet.of(Role.USER));
info.addStringPermissions(builder.build());
permissions = ImmutableSet.of(ADMIN_PERMISSION);
}
else
{
roles = ImmutableSet.of(Role.USER);
Builder<String> builder = ImmutableSet.builder();
collectGlobalPermissions(builder, user, groups);
collectRepositoryPermissions(builder, user, groups);
builder.add(canReadOwnUser(user));
builder.add(getUserAutocompletePermission());
builder.add(getGroupAutocompletePermission());
builder.add(getChangeOwnPasswordPermission(user));
permissions = builder.build();
}
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles);
info.addStringPermissions(permissions);
return info;
}
private boolean isAdmin(User user, GroupNames groups) {
boolean admin = user.isAdmin();
if (admin) {
logger.debug("user {} is marked as admin, because of the user flag", user.getName());
return true;
}
if (isUserAdminInConfiguration(user)) {
logger.debug("user {} is marked as admin, because of the admin user configuration", user.getName());
return true;
}
return isUserAdminInGroupConfiguration(user, groups);
}
private boolean isUserAdminInGroupConfiguration(User user, GroupNames groups) {
Set<String> adminGroups = configuration.getAdminGroups();
if (adminGroups != null && groups != null) {
for (String group : groups) {
if (adminGroups.contains(group)) {
logger.debug("user {} is marked as admin, because of the admin group configuration for group {}", user.getName(), group);
return true;
}
}
}
return false;
}
private boolean isUserAdminInConfiguration(User user) {
Set<String> adminUsers = configuration.getAdminUsers();
return adminUsers != null && adminUsers.contains(user.getName());
}
private String getGroupAutocompletePermission() {
return GroupPermissions.autocomplete().asShiroString();
}
@@ -404,8 +345,6 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
//~--- fields ---------------------------------------------------------------
private final ScmConfiguration configuration;
/** authorization cache */
private final Cache<CacheKey, AuthorizationInfo> cache;

View File

@@ -71,13 +71,6 @@ import java.util.List;
public class DefaultUserManager extends AbstractUserManager
{
/** Field description */
public static final String ADMIN_PATH = "/sonia/scm/config/admin-account.xml";
/** Field description */
public static final String ANONYMOUS_PATH =
"/sonia/scm/config/anonymous-account.xml";
/** Field description */
public static final String STORE_NAME = "users";
@@ -173,12 +166,6 @@ public class DefaultUserManager extends AbstractUserManager
@Override
public void init(SCMContextProvider context)
{
// create default account only, if no other account is available
if (userDAO.getAll().isEmpty())
{
createDefaultAccounts();
}
}
/**
@@ -457,28 +444,6 @@ public class DefaultUserManager extends AbstractUserManager
}
}
/**
* Method description
*
*/
private void createDefaultAccounts()
{
try
{
logger.info("create default accounts");
JAXBContext context = JAXBContext.newInstance(User.class);
Unmarshaller unmarshaller = context.createUnmarshaller();
createDefaultAccount(unmarshaller, ADMIN_PATH);
createDefaultAccount(unmarshaller, ANONYMOUS_PATH);
}
catch (JAXBException ex)
{
logger.error("could not create default accounts", ex);
}
}
//~--- fields ---------------------------------------------------------------
private final UserDAO userDAO;

View File

@@ -0,0 +1,8 @@
package sonia.scm.web.security;
final class AdministrationContextMarker {
static final AdministrationContextMarker MARKER = new AdministrationContextMarker();
private AdministrationContextMarker() {}
}

View File

@@ -0,0 +1,42 @@
package sonia.scm.web.security;
import com.google.common.collect.Sets;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.plugin.Extension;
import sonia.scm.security.Role;
@Extension
public class AdministrationContextRealm extends AuthorizingRealm {
private static final Logger LOG = LoggerFactory.getLogger(AdministrationContextRealm.class);
public AdministrationContextRealm() {
setName(DefaultAdministrationContext.REALM);
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
AdministrationContextMarker marker = principals.oneByType(AdministrationContextMarker.class);
if (marker == AdministrationContextMarker.MARKER) {
LOG.info("assign admin permissions to admin context user {}", principals.getPrimaryPrincipal());
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(Sets.newHashSet(Role.USER));
authorizationInfo.setStringPermissions(Sets.newHashSet("*"));
return authorizationInfo;
}
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) {
// we make no authentication we do only authorization
return null;
}
}

View File

@@ -75,7 +75,7 @@ public class DefaultAdministrationContext implements AdministrationContext
"/sonia/scm/web/security/system-account.xml";
/** Field description */
private static final String REALM = "AdminRealm";
static final String REALM = "AdminRealm";
/** the logger for DefaultAdministrationContext */
private static final Logger logger =
@@ -124,19 +124,7 @@ public class DefaultAdministrationContext implements AdministrationContext
if (ThreadContext.getSecurityManager() != null)
{
Subject subject = SecurityUtils.getSubject();
if (subject.hasRole(Role.ADMIN))
{
logger.debug(
"user is already an admin, we need no system account session, execute action {}",
action.getClass().getName());
action.run();
}
else
{
doRunAsInWebSessionContext(action);
}
doRunAsInWebSessionContext(action);
}
else
{
@@ -174,6 +162,7 @@ public class DefaultAdministrationContext implements AdministrationContext
collection.add(adminUser.getId(), REALM);
collection.add(adminUser, REALM);
collection.add(new GroupNames(), REALM);
collection.add(AdministrationContextMarker.MARKER, REALM);
return collection;
}

View File

@@ -41,8 +41,6 @@ public class ConfigDtoToScmConfigurationMapperTest {
assertTrue(config.isDisableGroupingGrid());
assertEquals("yyyy" , config.getDateFormat());
assertTrue(config.isAnonymousAccessEnabled());
assertTrue("adminGroups", config.getAdminGroups().containsAll(Arrays.asList(expectedGroups)));
assertTrue("adminUsers", config.getAdminUsers().containsAll(Arrays.asList(expectedUsers)));
assertEquals("baseurl" , config.getBaseUrl());
assertTrue(config.isForceBaseUrl());
assertEquals(41 , config.getLoginAttemptLimit());
@@ -66,8 +64,6 @@ public class ConfigDtoToScmConfigurationMapperTest {
configDto.setDisableGroupingGrid(true);
configDto.setDateFormat("yyyy");
configDto.setAnonymousAccessEnabled(true);
configDto.setAdminGroups(Sets.newSet(expectedGroups));
configDto.setAdminUsers(Sets.newSet(expectedUsers));
configDto.setBaseUrl("baseurl");
configDto.setForceBaseUrl(true);
configDto.setLoginAttemptLimit(41);

View File

@@ -125,27 +125,6 @@ public class ConfigResourceTest {
dispatcher.invoke(request, response);
}
@Test
@SubjectAware(username = "readWrite")
public void shouldFailForEmptyAdminUsers() throws URISyntaxException, IOException {
MockHttpRequest request = post("sonia/scm/api/v2/config-test-empty-admin-user.json");
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertEquals(HttpServletResponse.SC_BAD_REQUEST, response.getStatus());
}
@Test
@SubjectAware(username = "readWrite")
public void shouldFailForEmptyAdminGroups() throws URISyntaxException, IOException {
MockHttpRequest request = post("sonia/scm/api/v2/config-test-empty-admin-group.json");
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertEquals(HttpServletResponse.SC_BAD_REQUEST, response.getStatus());
}
@Test

View File

@@ -71,8 +71,6 @@ public class ScmConfigurationToConfigDtoMapperTest {
assertTrue(dto.isDisableGroupingGrid());
assertEquals("dd" , dto.getDateFormat());
assertTrue(dto.isAnonymousAccessEnabled());
assertTrue("adminGroups", dto.getAdminGroups().containsAll(Arrays.asList(expectedGroups)));
assertTrue("adminUsers", dto.getAdminUsers().containsAll(Arrays.asList(expectedUsers)));
assertEquals("baseurl" , dto.getBaseUrl());
assertTrue(dto.isForceBaseUrl());
assertEquals(1 , dto.getLoginAttemptLimit());
@@ -111,8 +109,6 @@ public class ScmConfigurationToConfigDtoMapperTest {
config.setDisableGroupingGrid(true);
config.setDateFormat("dd");
config.setAnonymousAccessEnabled(true);
config.setAdminGroups(Sets.newSet(expectedGroups));
config.setAdminUsers(Sets.newSet(expectedUsers));
config.setBaseUrl("baseurl");
config.setForceBaseUrl(true);
config.setLoginAttemptLimit(1);

View File

@@ -0,0 +1,94 @@
package sonia.scm.boot;
import com.google.common.collect.Lists;
import org.apache.shiro.authc.credential.PasswordService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.security.PermissionAssigner;
import sonia.scm.security.PermissionDescriptor;
import sonia.scm.user.User;
import sonia.scm.user.UserManager;
import sonia.scm.user.UserTestData;
import sonia.scm.web.security.AdministrationContext;
import java.util.Collection;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class SetupContextListenerTest {
@Mock
private AdministrationContext administrationContext;
@InjectMocks
private SetupContextListener setupContextListener;
@Mock
private UserManager userManager;
@Mock
private PasswordService passwordService;
@Mock
private PermissionAssigner permissionAssigner;
@InjectMocks
private SetupContextListener.SetupAction setupAction;
@BeforeEach
void setupObjectUnderTest() {
doAnswer(ic -> {
setupAction.run();
return null;
}).when(administrationContext).runAsAdmin(SetupContextListener.SetupAction.class);
}
@Test
void shouldCreateAdminAccountAndAssignPermissions() {
when(passwordService.encryptPassword("scmadmin")).thenReturn("secret");
setupContextListener.contextInitialized(null);
verifyAdminCreated();
verifyAdminPermissionsAssigned();
}
@Test
void shouldDoNothingOnSecondStart() {
List<User> users = Lists.newArrayList(UserTestData.createTrillian());
when(userManager.getAll()).thenReturn(users);
setupContextListener.contextInitialized(null);
verify(userManager, never()).create(any(User.class));
verify(permissionAssigner, never()).setPermissionsForUser(anyString(), any(Collection.class));
}
private void verifyAdminPermissionsAssigned() {
ArgumentCaptor<String> usernameCaptor = ArgumentCaptor.forClass(String.class);
ArgumentCaptor<Collection<PermissionDescriptor>> permissionCaptor = ArgumentCaptor.forClass(Collection.class);
verify(permissionAssigner).setPermissionsForUser(usernameCaptor.capture(), permissionCaptor.capture());
String username = usernameCaptor.getValue();
assertThat(username).isEqualTo("scmadmin");
PermissionDescriptor descriptor = permissionCaptor.getValue().iterator().next();
assertThat(descriptor.getValue()).isEqualTo("*");
}
private void verifyAdminCreated() {
ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
verify(userManager).create(userCaptor.capture());
User user = userCaptor.getValue();
assertThat(user.getName()).isEqualTo("scmadmin");
assertThat(user.getPassword()).isEqualTo("secret");
}
}

View File

@@ -142,7 +142,6 @@ public class GitLfsITCase {
dto.setDisplayName(user.getDisplayName());
dto.setType(user.getType());
dto.setActive(user.isActive());
dto.setAdmin(user.isAdmin());
dto.setPassword(user.getPassword());
createResource(adminClient, "users")
.accept("*/*")

View File

@@ -133,7 +133,6 @@ public class UserPermissionITCase extends AbstractPermissionITCaseBase<User>
"scm-admin@scm-manager.org");
user.setPassword("hallo123");
user.setAdmin(true);
user.setType("xml");
return user;

View File

@@ -1,10 +1,10 @@
/**
* Copyright (c) 2014, Sebastian Sdorra
* All rights reserved.
*
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
@@ -13,7 +13,7 @@
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
@@ -24,9 +24,9 @@
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
*
* http://bitbucket.org/sdorra/scm-manager
*
*
*/
package sonia.scm.security;
@@ -58,18 +58,18 @@ import static org.junit.Assert.assertTrue;
/**
* Unit tests for {@link AuthorizationChangedEventProducer}.
*
*
* @author Sebastian Sdorra
*/
public class AuthorizationChangedEventProducerTest {
private StoringAuthorizationChangedEventProducer producer;
@Before
public void setUpProducer() {
producer = new StoringAuthorizationChangedEventProducer();
}
/**
* Tests {@link AuthorizationChangedEventProducer#onEvent(sonia.scm.user.UserEvent)}.
*/
@@ -79,15 +79,15 @@ public class AuthorizationChangedEventProducerTest {
User user = UserTestData.createDent();
producer.onEvent(new UserEvent(HandlerEventType.BEFORE_CREATE, user));
assertEventIsNotFired();
producer.onEvent(new UserEvent(HandlerEventType.CREATE, user));
assertUserEventIsFired("dent");
}
private void assertEventIsNotFired(){
assertNull(producer.event);
}
private void assertUserEventIsFired(String username){
assertNotNull(producer.event);
assertTrue(producer.event.isEveryUserAffected());
@@ -102,28 +102,28 @@ public class AuthorizationChangedEventProducerTest {
/**
* Tests {@link AuthorizationChangedEventProducer#onEvent(sonia.scm.user.UserEvent)} with modified user.
*/
@Test
@Test
public void testOnUserModificationEvent()
{
User user = UserTestData.createDent();
User userModified = UserTestData.createDent();
userModified.setDisplayName("Super Dent");
producer.onEvent(new UserModificationEvent(HandlerEventType.BEFORE_CREATE, userModified, user));
assertEventIsNotFired();
producer.onEvent(new UserModificationEvent(HandlerEventType.CREATE, userModified, user));
assertEventIsNotFired();
userModified.setAdmin(true);
userModified.setActive(false);
producer.onEvent(new UserModificationEvent(HandlerEventType.BEFORE_CREATE, userModified, user));
assertEventIsNotFired();
producer.onEvent(new UserModificationEvent(HandlerEventType.CREATE, userModified, user));
assertUserEventIsFired("dent");
}
/**
* Tests {@link AuthorizationChangedEventProducer#onEvent(sonia.scm.group.GroupEvent)}.
*/
@@ -133,11 +133,11 @@ public class AuthorizationChangedEventProducerTest {
Group group = new Group("xml", "base");
producer.onEvent(new GroupEvent(HandlerEventType.BEFORE_CREATE, group));
assertEventIsNotFired();
producer.onEvent(new GroupEvent(HandlerEventType.CREATE, group));
assertGlobalEventIsFired();
}
/**
* Tests {@link AuthorizationChangedEventProducer#onEvent(sonia.scm.group.GroupEvent)} with modified groups.
*/
@@ -148,15 +148,15 @@ public class AuthorizationChangedEventProducerTest {
Group modifiedGroup = new Group("xml", "base");
producer.onEvent(new GroupModificationEvent(HandlerEventType.BEFORE_MODIFY, modifiedGroup, group));
assertEventIsNotFired();
producer.onEvent(new GroupModificationEvent(HandlerEventType.MODIFY, modifiedGroup, group));
assertEventIsNotFired();
modifiedGroup.add("test");
producer.onEvent(new GroupModificationEvent(HandlerEventType.MODIFY, modifiedGroup, group));
assertGlobalEventIsFired();
}
/**
* Tests {@link AuthorizationChangedEventProducer#onEvent(sonia.scm.repository.RepositoryEvent)}.
*/
@@ -166,13 +166,13 @@ public class AuthorizationChangedEventProducerTest {
Repository repository = RepositoryTestData.createHeartOfGold();
producer.onEvent(new RepositoryEvent(HandlerEventType.BEFORE_CREATE, repository));
assertEventIsNotFired();
producer.onEvent(new RepositoryEvent(HandlerEventType.CREATE, repository));
assertGlobalEventIsFired();
}
/**
* Tests {@link AuthorizationChangedEventProducer#onEvent(sonia.scm.repository.RepositoryEvent)} with modified
* Tests {@link AuthorizationChangedEventProducer#onEvent(sonia.scm.repository.RepositoryEvent)} with modified
* repository.
*/
@Test
@@ -224,11 +224,11 @@ public class AuthorizationChangedEventProducerTest {
producer.onEvent(new RepositoryModificationEvent(HandlerEventType.CREATE, repositoryModified, repository));
assertEventIsNotFired();
}
private void resetStoredEvent(){
producer.event = null;
}
/**
* Tests {@link AuthorizationChangedEventProducer#onEvent(AssignedPermissionEvent)}.
*/
@@ -240,33 +240,33 @@ public class AuthorizationChangedEventProducerTest {
);
producer.onEvent(new AssignedPermissionEvent(HandlerEventType.BEFORE_CREATE, groupPermission));
assertEventIsNotFired();
producer.onEvent(new AssignedPermissionEvent(HandlerEventType.CREATE, groupPermission));
assertGlobalEventIsFired();
resetStoredEvent();
StoredAssignedPermission userPermission = new StoredAssignedPermission(
"123", new AssignedPermission("trillian", false, "repository:read:*")
);
producer.onEvent(new AssignedPermissionEvent(HandlerEventType.BEFORE_CREATE, userPermission));
assertEventIsNotFired();
resetStoredEvent();
producer.onEvent(new AssignedPermissionEvent(HandlerEventType.CREATE, userPermission));
assertUserEventIsFired("trillian");
}
private static class StoringAuthorizationChangedEventProducer extends AuthorizationChangedEventProducer {
private AuthorizationChangedEvent event;
@Override
protected void sendEvent(AuthorizationChangedEvent event) {
this.event = event;
}
}
}

View File

@@ -78,8 +78,6 @@ import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class DefaultAuthorizationCollectorTest {
private ScmConfiguration configuration;
@Mock
private Cache cache;
@@ -103,38 +101,7 @@ public class DefaultAuthorizationCollectorTest {
@Before
public void setUp(){
when(cacheManager.getCache(Mockito.any(String.class))).thenReturn(cache);
configuration = new ScmConfiguration();
collector = new DefaultAuthorizationCollector(configuration, cacheManager, repositoryDAO, securitySystem);
}
@Test
@SubjectAware(
configuration = "classpath:sonia/scm/shiro-001.ini"
)
public void shouldGetAdminPrivilegedByConfiguration() {
configuration.setAdminUsers(ImmutableSet.of("trillian"));
authenticate(UserTestData.createTrillian(), "main");
AuthorizationInfo authInfo = collector.collect();
assertIsAdmin(authInfo);
}
private void assertIsAdmin(AuthorizationInfo authInfo) {
assertThat(authInfo.getRoles(), Matchers.containsInAnyOrder(Role.USER, Role.ADMIN));
assertThat(authInfo.getObjectPermissions(), nullValue());
assertThat(authInfo.getStringPermissions(), Matchers.contains("*"));
}
@Test
@SubjectAware(
configuration = "classpath:sonia/scm/shiro-001.ini"
)
public void shouldGetAdminPrivilegedByGroupConfiguration() {
configuration.setAdminGroups(ImmutableSet.of("heartOfGold"));
authenticate(UserTestData.createTrillian(), "heartOfGold");
AuthorizationInfo authInfo = collector.collect();
assertIsAdmin(authInfo);
collector = new DefaultAuthorizationCollector(cacheManager, repositoryDAO, securitySystem);
}
/**
@@ -197,22 +164,6 @@ public class DefaultAuthorizationCollectorTest {
assertThat(authInfo.getObjectPermissions(), nullValue());
}
/**
* Tests {@link AuthorizationCollector#collect()} as admin.
*/
@Test
@SubjectAware(
configuration = "classpath:sonia/scm/shiro-001.ini"
)
public void testCollectAsAdmin() {
User trillian = UserTestData.createTrillian();
trillian.setAdmin(true);
authenticate(trillian, "main");
AuthorizationInfo authInfo = collector.collect();
assertIsAdmin(authInfo);
}
/**
* Tests {@link AuthorizationCollector#collect()} with repository permissions.
*/

View File

@@ -67,7 +67,7 @@ import org.junit.Rule;
)
public class DefaultUserManagerTest extends UserManagerTestBase
{
@Rule
public ShiroRule shiro = new ShiroRule();
@@ -97,39 +97,6 @@ public class DefaultUserManagerTest extends UserManagerTestBase
when(userDAO.get("trillian")).thenReturn(trillian);
}
/**
* Method description
*
*/
@Test
public void testDefaultAccountAfterFristStart()
{
List<User> users = Lists.newArrayList(new User("tuser"));
when(userDAO.getAll()).thenReturn(users);
UserManager userManager = new DefaultUserManager(userDAO);
userManager.init(contextProvider);
verify(userDAO, never()).add(any(User.class));
}
/**
* Method description
*
*/
@Test
@SuppressWarnings("unchecked")
public void testDefaultAccountCreation()
{
when(userDAO.getAll()).thenReturn(Collections.EMPTY_LIST);
UserManager userManager = new DefaultUserManager(userDAO);
userManager.init(contextProvider);
verify(userDAO, times(2)).add(any(User.class));
}
@Test(expected = InvalidPasswordException.class)
public void shouldFailChangePasswordForWrongOldPassword() {
UserManager userManager = new DefaultUserManager(userDAO);

View File

@@ -0,0 +1,34 @@
package sonia.scm.web.security;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class AdministrationContextRealmTest {
private AdministrationContextRealm realm = new AdministrationContextRealm();
@Test
void shouldAssignAdminPermissions() {
SimplePrincipalCollection collection = new SimplePrincipalCollection();
collection.add("scm-system", DefaultAdministrationContext.REALM);
collection.add(AdministrationContextMarker.MARKER, DefaultAdministrationContext.REALM);
AuthorizationInfo authorizationInfo = realm.doGetAuthorizationInfo(collection);
assertThat(authorizationInfo.getStringPermissions()).containsOnly("*");
}
@Test
void shouldReturnNull() {
SimplePrincipalCollection collection = new SimplePrincipalCollection();
collection.add("scm-system", DefaultAdministrationContext.REALM);
AuthorizationInfo authorizationInfo = realm.doGetAuthorizationInfo(collection);
assertThat(authorizationInfo).isNull();
}
}

View File

@@ -1,3 +0,0 @@
{
"adminGroups": [""]
}

View File

@@ -1,3 +0,0 @@
{
"adminUsers": [""]
}