Merged feature/changes_for_ssh_plugin into 2.0.0-m3

This commit is contained in:
René Pfeuffer
2019-02-20 13:51:47 +01:00
12 changed files with 268 additions and 94 deletions

View File

@@ -77,15 +77,13 @@ public final class ScmState
* @param repositoryTypes available repository types
* @param defaultUserType default user type
* @param clientConfig client configuration
* @param assignedPermission assigned permissions
* @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, List<String> assignedPermission,
Collection<PermissionDescriptor> availablePermissions)
ScmClientConfig clientConfig, Collection<PermissionDescriptor> availablePermissions)
{
this.version = version;
this.user = user;
@@ -94,24 +92,11 @@ public final class ScmState
this.repositoryTypes = repositoryTypes;
this.clientConfig = clientConfig;
this.defaultUserType = defaultUserType;
this.assignedPermissions = assignedPermission;
this.availablePermissions = availablePermissions;
}
//~--- get methods ----------------------------------------------------------
/**
* Return a list of assigned permissions.
*
*
* @return list of assigned permissions
* @since 1.31
*/
public List<String> getAssignedPermissions()
{
return assignedPermissions;
}
/**
* Returns a list of available global permissions.
*
@@ -225,9 +210,6 @@ public final class ScmState
/** authentication token */
private String token;
/** Field description */
private List<String> assignedPermissions;
/**
* Avaliable global permission
* @since 1.31

View File

@@ -74,20 +74,17 @@ public final class ScmStateFactory
* @param repositoryManger repository manager
* @param userManager user manager
* @param securitySystem security system
* @param authorizationCollector authorization collector
*/
@Inject
public ScmStateFactory(SCMContextProvider contextProvider,
ScmConfiguration configuration, RepositoryManager repositoryManger,
UserManager userManager, SecuritySystem securitySystem,
AuthorizationCollector authorizationCollector)
UserManager userManager, SecuritySystem securitySystem)
{
this.contextProvider = contextProvider;
this.configuration = configuration;
this.repositoryManger = repositoryManger;
this.userManager = userManager;
this.securitySystem = securitySystem;
this.authorizationCollector = authorizationCollector;
}
//~--- methods --------------------------------------------------------------
@@ -101,8 +98,7 @@ public final class ScmStateFactory
@SuppressWarnings("unchecked")
public ScmState createAnonymousState()
{
return createState(SCMContext.ANONYMOUS, Collections.EMPTY_LIST, null,
Collections.EMPTY_LIST, Collections.EMPTY_LIST);
return createState(SCMContext.ANONYMOUS, Collections.EMPTY_LIST, null, Collections.EMPTY_LIST);
}
/**
@@ -141,15 +137,11 @@ public final class ScmStateFactory
ap = securitySystem.getAvailablePermissions();
}
List<String> permissions =
ImmutableList.copyOf(
authorizationCollector.collect().getStringPermissions());
return createState(user, groups.getCollection(), token, permissions, ap);
return createState(user, groups.getCollection(), token, ap);
}
private ScmState createState(User user, Collection<String> groups,
String token, List<String> assignedPermissions,
String token,
Collection<PermissionDescriptor> availablePermissions)
{
User u = user.clone();
@@ -159,15 +151,11 @@ public final class ScmStateFactory
return new ScmState(contextProvider.getVersion(), u, groups, token,
repositoryManger.getConfiguredTypes(), userManager.getDefaultType(),
new ScmClientConfig(configuration), assignedPermissions,
availablePermissions);
new ScmClientConfig(configuration), availablePermissions);
}
//~--- fields ---------------------------------------------------------------
/** authorization collector */
private final AuthorizationCollector authorizationCollector;
/** configuration */
private final ScmConfiguration configuration;

View File

@@ -3,10 +3,29 @@ package sonia.scm.repository.api;
import sonia.scm.plugin.ExtensionPoint;
import sonia.scm.repository.Repository;
/**
* Provider for scm native protocols.
*
* @param <T> type of protocol
*
* @since 2.0.0
*/
@ExtensionPoint(multi = true)
public interface ScmProtocolProvider<T extends ScmProtocol> {
/**
* Returns type of repository (e.g.: git, svn, hg, etc.)
*
* @return name of type
*/
String getType();
/**
* Returns protocol for the given repository.
*
* @param repository repository
*
* @return protocol for repository
*/
T get(Repository repository);
}

View File

@@ -34,6 +34,7 @@ package sonia.scm.security;
//~--- non-JDK imports --------------------------------------------------------
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.subject.PrincipalCollection;
import sonia.scm.plugin.ExtensionPoint;
/**
@@ -42,15 +43,16 @@ import sonia.scm.plugin.ExtensionPoint;
* @author Sebastian Sdorra
* @since 2.0.0
*/
@ExtensionPoint(multi = false)
@ExtensionPoint
public interface AuthorizationCollector
{
/**
* Returns {@link AuthorizationInfo} for the authenticated user.
*
* @param principalCollection collected principals
*
* @return {@link AuthorizationInfo} for authenticated user
*/
public AuthorizationInfo collect();
AuthorizationInfo collect(PrincipalCollection principalCollection);
}

View File

@@ -0,0 +1,57 @@
//@flow
import React from "react";
import { translate } from "react-i18next";
import type { Repository } from "@scm-manager/ui-types";
type Props = {
url: string,
repository: Repository,
// context props
t: (string) => string
};
class CloneInformation extends React.Component<Props> {
render() {
const { url, repository, t } = this.props;
return (
<div>
<h4>{t("scm-git-plugin.information.clone")}</h4>
<pre>
<code>git clone {url}</code>
</pre>
<h4>{t("scm-git-plugin.information.create")}</h4>
<pre>
<code>
git init {repository.name}
<br />
echo "# {repository.name}" > README.md
<br />
git add README.md
<br />
git commit -m "added readme"
<br />
git remote add origin {url}
<br />
git push -u origin master
<br />
</code>
</pre>
<h4>{t("scm-git-plugin.information.replace")}</h4>
<pre>
<code>
git remote add origin {url}
<br />
git push -u origin master
<br />
</code>
</pre>
</div>
);
}
}
export default translate("plugins")(CloneInformation);

View File

@@ -1,59 +1,108 @@
//@flow
import React from "react";
import { repositories } from "@scm-manager/ui-components";
import { ButtonGroup, Button } from "@scm-manager/ui-components";
import type { Repository } from "@scm-manager/ui-types";
import { translate } from "react-i18next";
import CloneInformation from "./CloneInformation";
import type { Link } from "@scm-manager/ui-types";
import injectSheets from "react-jss";
const styles = {
protocols: {
position: "relative"
},
switcher: {
position: "absolute",
top: 0,
right: 0
}
};
type Props = {
repository: Repository,
t: string => string
// context props
classes: Object
}
class ProtocolInformation extends React.Component<Props> {
type State = {
selected?: Link
};
render() {
const { repository, t } = this.props;
const href = repositories.getProtocolLinkByType(repository, "http");
if (!href) {
return null;
function selectHttpOrFirst(repository: Repository) {
const protocols = repository._links["protocol"] || [];
for (let protocol of protocols) {
if (protocol.name === "http") {
return protocol;
}
}
if (protocols.length > 0) {
return protocols[0];
}
return undefined;
}
class ProtocolInformation extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
selected: selectHttpOrFirst(props.repository)
};
}
selectProtocol = (protocol: Link) => {
this.setState({
selected: protocol
});
};
renderProtocolButton = (protocol: Link) => {
const name = protocol.name || "unknown";
let color = null;
const { selected } = this.state;
if ( selected && protocol.name === selected.name ) {
color = "link is-selected";
}
return (
<div>
<h4>{t("scm-git-plugin.information.clone")}</h4>
<pre>
<code>git clone {href}</code>
</pre>
<h4>{t("scm-git-plugin.information.create")}</h4>
<pre>
<code>
git init {repository.name}
<br />
echo "# {repository.name}" > README.md
<br />
git add README.md
<br />
git commit -m "added readme"
<br />
git remote add origin {href}
<br />
git push -u origin master
<br />
</code>
</pre>
<h4>{t("scm-git-plugin.information.replace")}</h4>
<pre>
<code>
git remote add origin {href}
<br />
git push -u origin master
<br />
</code>
</pre>
</div>
<Button color={ color } action={() => this.selectProtocol(protocol)}>
{name.toUpperCase()}
</Button>
);
};
render() {
const { repository, classes } = this.props;
const protocols = repository._links["protocol"];
if (!protocols || protocols.length === 0) {
return null;
}
if (protocols.length === 1) {
return <CloneInformation url={protocols[0].href} repository={repository} />;
}
const { selected } = this.state;
let cloneInformation = null;
if (selected) {
cloneInformation = <CloneInformation repository={repository} url={selected.href} />;
}
return (
<div className={classes.protocols}>
<ButtonGroup className={classes.switcher}>
{protocols.map(this.renderProtocolButton)}
</ButtonGroup>
{ cloneInformation }
</div>
);
}
}
export default translate("plugins")(ProtocolInformation);
export default injectSheets(styles)(ProtocolInformation);

View File

@@ -73,6 +73,11 @@ class Profile extends React.Component<Props, State> {
path={`${url}/settings/password`}
render={() => <ChangeUserPassword me={me} />}
/>
<ExtensionPoint
name="profile.route"
props={extensionProps}
renderAll={true}
/>
</div>
<div className="column">
<Navigation>

View File

@@ -105,6 +105,11 @@ class SingleUser extends React.Component<Props> {
/>
)}
/>
<ExtensionPoint
name="user.route"
props={extensionProps}
renderAll={true}
/>
</div>
<div className="column">
<Navigation>

View File

@@ -36,6 +36,7 @@ package sonia.scm.security;
//~--- non-JDK imports --------------------------------------------------------
import com.github.legman.Subscribe;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSet;
@@ -118,8 +119,8 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
*
* @return
*/
@Override
public AuthorizationInfo collect()
@VisibleForTesting
AuthorizationInfo collect()
{
AuthorizationInfo authorizationInfo;
Subject subject = SecurityUtils.getSubject();
@@ -143,6 +144,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
*
* @return
*/
@Override
public AuthorizationInfo collect(PrincipalCollection principals)
{
Preconditions.checkNotNull(principals, "principals parameter is required");

View File

@@ -42,9 +42,11 @@ import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.PasswordMatcher;
import org.apache.shiro.authc.credential.PasswordService;
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.apache.shiro.subject.SimplePrincipalCollection;
import sonia.scm.group.GroupNames;
import sonia.scm.plugin.Extension;
@@ -56,6 +58,8 @@ import javax.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Set;
/**
* Default authorizing realm.
*
@@ -85,14 +89,13 @@ public class DefaultRealm extends AuthorizingRealm
*
*
* @param service
* @param collector
* @param authorizationCollectors
* @param helperFactory
*/
@Inject
public DefaultRealm(PasswordService service,
DefaultAuthorizationCollector collector, DAORealmHelperFactory helperFactory)
public DefaultRealm(PasswordService service, Set<AuthorizationCollector> authorizationCollectors, DAORealmHelperFactory helperFactory)
{
this.collector = collector;
this.authorizationCollectors = authorizationCollectors;
this.helper = helperFactory.create(REALM);
PasswordMatcher matcher = new PasswordMatcher();
@@ -133,8 +136,7 @@ public class DefaultRealm extends AuthorizingRealm
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals)
{
AuthorizationInfo info = collector.collect(principals);
AuthorizationInfo info = collectors(principals);
Scope scope = principals.oneByType(Scope.class);
if (scope != null && ! scope.isEmpty()) {
LOG.trace("filter permissions by scope {}", scope);
@@ -144,13 +146,36 @@ public class DefaultRealm extends AuthorizingRealm
}
return filtered;
} else if (LOG.isTraceEnabled()) {
LOG.trace("principal does not contain scope informations, returning all permissions");
LOG.trace("principal does not contain scope information, returning all permissions");
log(principals, info, null);
}
return info;
}
private AuthorizationInfo collectors(PrincipalCollection principals) {
SimpleAuthorizationInfo merged = new SimpleAuthorizationInfo();
for (AuthorizationCollector collector : authorizationCollectors) {
AuthorizationInfo authorizationInfo = collector.collect(principals);
merge(merged, authorizationInfo);
}
return merged;
}
private void merge(SimpleAuthorizationInfo merged, AuthorizationInfo authorizationInfo) {
if (authorizationInfo != null) {
if (authorizationInfo.getRoles() != null) {
merged.addRoles(authorizationInfo.getRoles());
}
if (authorizationInfo.getObjectPermissions() != null) {
merged.addObjectPermissions(authorizationInfo.getObjectPermissions());
}
if (authorizationInfo.getStringPermissions() != null) {
merged.addStringPermissions(authorizationInfo.getStringPermissions());
}
}
}
private void log( PrincipalCollection collection, AuthorizationInfo original, AuthorizationInfo filtered ) {
StringBuilder buffer = new StringBuilder("authorization summary: ");
@@ -190,8 +215,8 @@ public class DefaultRealm extends AuthorizingRealm
//~--- fields ---------------------------------------------------------------
/** default authorization collector */
private final DefaultAuthorizationCollector collector;
/** set of authorization collector */
private final Set<AuthorizationCollector> authorizationCollectors;
/** realm helper */
private final DAORealmHelper helper;

View File

@@ -205,7 +205,7 @@ private long calculateAverage(List<Long> times) {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return authzCollector.collect();
return authzCollector.collect(principals);
}
}

View File

@@ -71,7 +71,11 @@ import static org.mockito.Mockito.*;
//~--- JDK imports ------------------------------------------------------------
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.Permission;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
@@ -132,6 +136,36 @@ public class DefaultRealmTest
assertThat(realmsAutz.getStringPermissions(), Matchers.contains("repository:*"));
}
@Test
public void testGetAuthorizationInfoWithMultipleAuthorizationCollectors(){
SimplePrincipalCollection col = new SimplePrincipalCollection();
col.add(Scope.empty(), DefaultRealm.REALM);
SimpleAuthorizationInfo collectedFromDefault = new SimpleAuthorizationInfo();
collectedFromDefault.addStringPermission("repository:*");
when(collector.collect(col)).thenReturn(collectedFromDefault);
SimpleAuthorizationInfo collectedFromSecond = new SimpleAuthorizationInfo();
collectedFromSecond.addStringPermission("user:*");
collectedFromSecond.addRole("awesome");
AuthorizationCollector secondCollector = principalCollection -> collectedFromSecond;
authorizationCollectors.add(secondCollector);
SimpleAuthorizationInfo collectedFromThird = new SimpleAuthorizationInfo();
Permission permission = p -> false;
collectedFromThird.addObjectPermission(permission);
collectedFromThird.addRole("awesome");
AuthorizationCollector thirdCollector = principalCollection -> collectedFromThird;
authorizationCollectors.add(thirdCollector);
AuthorizationInfo realmsAuthz = realm.doGetAuthorizationInfo(col);
assertThat(realmsAuthz.getObjectPermissions(), contains(permission));
assertThat(realmsAuthz.getStringPermissions(), containsInAnyOrder("repository:*", "user:*"));
assertThat(realmsAuthz.getRoles(), Matchers.contains("awesome"));
}
/**
* Tests {@link DefaultRealm#doGetAuthorizationInfo(PrincipalCollection)} with empty scope.
*/
@@ -284,7 +318,11 @@ public class DefaultRealmTest
// use a small number of iterations for faster test execution
hashService.setHashIterations(512);
service.setHashService(hashService);
realm = new DefaultRealm(service, collector, helperFactory);
authorizationCollectors = new HashSet<>();
authorizationCollectors.add(collector);
realm = new DefaultRealm(service, authorizationCollectors, helperFactory);
// set permission resolver
realm.setPermissionResolver(new WildcardPermissionResolver());
@@ -358,6 +396,8 @@ public class DefaultRealmTest
@Mock
private DefaultAuthorizationCollector collector;
private Set<AuthorizationCollector> authorizationCollectors;
@Mock
private LoginAttemptHandler loginAttemptHandler;