mirror of
				https://github.com/scm-manager/scm-manager.git
				synced 2025-11-03 20:15:52 +01:00 
			
		
		
		
	Merge with 2.0.0-m3
This commit is contained in:
		@@ -80,8 +80,20 @@ public interface AccessToken {
 | 
			
		||||
   */
 | 
			
		||||
  Date getExpiration();
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns refresh expiration of token.
 | 
			
		||||
   *
 | 
			
		||||
   * @return refresh expiration
 | 
			
		||||
   */
 | 
			
		||||
  Optional<Date> getRefreshExpiration();
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns id of the parent key.
 | 
			
		||||
   *
 | 
			
		||||
   * @return parent key id
 | 
			
		||||
   */
 | 
			
		||||
  Optional<String> getParentKey();
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns the scope of the token. The scope is able to reduce the permissions of the subject in the context of this
 | 
			
		||||
   * token. For example we could issue a token which can only be used to read a single repository. for more informations
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,30 @@
 | 
			
		||||
package sonia.scm.security;
 | 
			
		||||
 | 
			
		||||
import javax.servlet.http.HttpServletRequest;
 | 
			
		||||
import javax.servlet.http.HttpServletResponse;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Generates cookies and invalidates access token cookies.
 | 
			
		||||
 *
 | 
			
		||||
 * @author Sebastian Sdorra
 | 
			
		||||
 * @since 2.0.0
 | 
			
		||||
 */
 | 
			
		||||
public interface AccessTokenCookieIssuer {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Creates a cookie for token authentication and attaches it to the response.
 | 
			
		||||
   *
 | 
			
		||||
   * @param request http servlet request
 | 
			
		||||
   * @param response http servlet response
 | 
			
		||||
   * @param accessToken access token
 | 
			
		||||
   */
 | 
			
		||||
  void authenticate(HttpServletRequest request, HttpServletResponse response, AccessToken accessToken);
 | 
			
		||||
  /**
 | 
			
		||||
   * Invalidates the authentication cookie.
 | 
			
		||||
   *
 | 
			
		||||
   * @param request http servlet request
 | 
			
		||||
   * @param response http servlet response
 | 
			
		||||
   */
 | 
			
		||||
  void invalidate(HttpServletRequest request, HttpServletResponse response);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -164,7 +164,7 @@ public class DefaultCipherHandler implements CipherHandler {
 | 
			
		||||
    String result = null;
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      byte[] encodedInput = Base64.getDecoder().decode(value);
 | 
			
		||||
      byte[] encodedInput = Base64.getUrlDecoder().decode(value);
 | 
			
		||||
      byte[] salt = new byte[SALT_LENGTH];
 | 
			
		||||
      byte[] encoded = new byte[encodedInput.length - SALT_LENGTH];
 | 
			
		||||
 | 
			
		||||
@@ -221,7 +221,7 @@ public class DefaultCipherHandler implements CipherHandler {
 | 
			
		||||
      System.arraycopy(salt, 0, result, 0, SALT_LENGTH);
 | 
			
		||||
      System.arraycopy(encodedInput, 0, result, SALT_LENGTH,
 | 
			
		||||
        result.length - SALT_LENGTH);
 | 
			
		||||
      res = new String(Base64.getEncoder().encode(result), ENCODING);
 | 
			
		||||
      res = new String(Base64.getUrlEncoder().encode(result), ENCODING);
 | 
			
		||||
    } catch (IOException | GeneralSecurityException ex) {
 | 
			
		||||
      throw new CipherException("could not encode string", ex);
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										25
									
								
								scm-core/src/main/java/sonia/scm/xml/XmlInstantAdapter.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								scm-core/src/main/java/sonia/scm/xml/XmlInstantAdapter.java
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,25 @@
 | 
			
		||||
package sonia.scm.xml;
 | 
			
		||||
 | 
			
		||||
import javax.xml.bind.annotation.adapters.XmlAdapter;
 | 
			
		||||
import java.time.Instant;
 | 
			
		||||
import java.time.format.DateTimeFormatter;
 | 
			
		||||
import java.time.temporal.TemporalAccessor;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * JAXB adapter for {@link Instant} objects.
 | 
			
		||||
 *
 | 
			
		||||
 * @since 2.0.0
 | 
			
		||||
 */
 | 
			
		||||
public class XmlInstantAdapter extends XmlAdapter<String, Instant> {
 | 
			
		||||
 | 
			
		||||
  @Override
 | 
			
		||||
  public String marshal(Instant instant) {
 | 
			
		||||
    return DateTimeFormatter.ISO_INSTANT.format(instant);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Override
 | 
			
		||||
  public Instant unmarshal(String text) {
 | 
			
		||||
    TemporalAccessor parsed = DateTimeFormatter.ISO_INSTANT.parse(text);
 | 
			
		||||
    return Instant.from(parsed);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,47 @@
 | 
			
		||||
package sonia.scm.xml;
 | 
			
		||||
 | 
			
		||||
import org.junit.jupiter.api.Test;
 | 
			
		||||
import org.junit.jupiter.api.extension.ExtendWith;
 | 
			
		||||
import org.junitpioneer.jupiter.TempDirectory;
 | 
			
		||||
 | 
			
		||||
import javax.xml.bind.JAXB;
 | 
			
		||||
import javax.xml.bind.annotation.XmlAccessType;
 | 
			
		||||
import javax.xml.bind.annotation.XmlAccessorType;
 | 
			
		||||
import javax.xml.bind.annotation.XmlRootElement;
 | 
			
		||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
 | 
			
		||||
import java.nio.file.Path;
 | 
			
		||||
import java.time.Instant;
 | 
			
		||||
 | 
			
		||||
import static org.junit.jupiter.api.Assertions.*;
 | 
			
		||||
 | 
			
		||||
@ExtendWith(TempDirectory.class)
 | 
			
		||||
class XmlInstantAdapterTest {
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  void shouldMarshalAndUnmarshalInstant(@TempDirectory.TempDir Path tempDirectory) {
 | 
			
		||||
    Path path = tempDirectory.resolve("instant.xml");
 | 
			
		||||
 | 
			
		||||
    Instant instant = Instant.now();
 | 
			
		||||
    InstantObject object = new InstantObject(instant);
 | 
			
		||||
    JAXB.marshal(object, path.toFile());
 | 
			
		||||
 | 
			
		||||
    InstantObject unmarshaled = JAXB.unmarshal(path.toFile(), InstantObject.class);
 | 
			
		||||
    assertEquals(instant, unmarshaled.instant);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @XmlRootElement(name = "instant-object")
 | 
			
		||||
  @XmlAccessorType(XmlAccessType.FIELD)
 | 
			
		||||
  public static class InstantObject {
 | 
			
		||||
 | 
			
		||||
    @XmlJavaTypeAdapter(XmlInstantAdapter.class)
 | 
			
		||||
    private Instant instant;
 | 
			
		||||
 | 
			
		||||
    public InstantObject() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    InstantObject(Instant instant) {
 | 
			
		||||
      this.instant = instant;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -35,8 +35,10 @@ package sonia.scm.repository.spi;
 | 
			
		||||
//~--- non-JDK imports --------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
import org.junit.After;
 | 
			
		||||
import org.junit.Before;
 | 
			
		||||
import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider;
 | 
			
		||||
import sonia.scm.repository.GitRepositoryConfig;
 | 
			
		||||
import sonia.scm.store.InMemoryConfigurationStore;
 | 
			
		||||
import sonia.scm.store.InMemoryConfigurationStoreFactory;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -69,7 +71,7 @@ public class AbstractGitCommandTestBase extends ZippedRepositoryTestBase
 | 
			
		||||
  {
 | 
			
		||||
    if (context == null)
 | 
			
		||||
    {
 | 
			
		||||
      context = new GitContext(repositoryDirectory, repository, new GitRepositoryConfigStoreProvider(new InMemoryConfigurationStoreFactory()));
 | 
			
		||||
      context = new GitContext(repositoryDirectory, repository, new GitRepositoryConfigStoreProvider(InMemoryConfigurationStoreFactory.create()));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return context;
 | 
			
		||||
 
 | 
			
		||||
@@ -35,55 +35,33 @@ package sonia.scm.store;
 | 
			
		||||
 | 
			
		||||
//~--- non-JDK imports --------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
import java.util.HashMap;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.Objects;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * In memory configuration store factory for testing purposes.
 | 
			
		||||
 *
 | 
			
		||||
 * Use {@link #create()} to get a store that creates the same store on each request.
 | 
			
		||||
 * 
 | 
			
		||||
 * @author Sebastian Sdorra
 | 
			
		||||
 */
 | 
			
		||||
public class InMemoryConfigurationStoreFactory implements ConfigurationStoreFactory {
 | 
			
		||||
 | 
			
		||||
  private static final Map<Key, InMemoryConfigurationStore> STORES = new HashMap<>();
 | 
			
		||||
  private ConfigurationStore store;
 | 
			
		||||
 | 
			
		||||
  public static ConfigurationStoreFactory create() {
 | 
			
		||||
    return new InMemoryConfigurationStoreFactory(new InMemoryConfigurationStore());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public InMemoryConfigurationStoreFactory() {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public InMemoryConfigurationStoreFactory(ConfigurationStore store) {
 | 
			
		||||
    this.store = store;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Override
 | 
			
		||||
  public ConfigurationStore getStore(TypedStoreParameters storeParameters) {
 | 
			
		||||
    Key key = new Key(storeParameters.getType(), storeParameters.getName(), storeParameters.getRepository() == null? "-": storeParameters.getRepository().getId());
 | 
			
		||||
    if (STORES.containsKey(key)) {
 | 
			
		||||
      return STORES.get(key);
 | 
			
		||||
    } else {
 | 
			
		||||
      InMemoryConfigurationStore<Object> store = new InMemoryConfigurationStore<>();
 | 
			
		||||
      STORES.put(key, store);
 | 
			
		||||
    if (store != null) {
 | 
			
		||||
      return store;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private static class Key {
 | 
			
		||||
    private final Class type;
 | 
			
		||||
    private final String name;
 | 
			
		||||
    private final String id;
 | 
			
		||||
 | 
			
		||||
    public Key(Class type, String name, String id) {
 | 
			
		||||
      this.type = type;
 | 
			
		||||
      this.name = name;
 | 
			
		||||
      this.id = id;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean equals(Object o) {
 | 
			
		||||
      if (this == o) return true;
 | 
			
		||||
      if (o == null || getClass() != o.getClass()) return false;
 | 
			
		||||
      Key key = (Key) o;
 | 
			
		||||
      return Objects.equals(type, key.type) &&
 | 
			
		||||
        Objects.equals(name, key.name) &&
 | 
			
		||||
        Objects.equals(id, key.id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public int hashCode() {
 | 
			
		||||
      return Objects.hash(type, name, id);
 | 
			
		||||
    }
 | 
			
		||||
    return new InMemoryConfigurationStore<>();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,53 @@
 | 
			
		||||
package sonia.scm.store;
 | 
			
		||||
 | 
			
		||||
import sonia.scm.security.KeyGenerator;
 | 
			
		||||
import sonia.scm.security.UUIDKeyGenerator;
 | 
			
		||||
 | 
			
		||||
import java.util.Collections;
 | 
			
		||||
import java.util.HashMap;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * In memory store implementation of {@link DataStore}.
 | 
			
		||||
 *
 | 
			
		||||
 * @author Sebastian Sdorra
 | 
			
		||||
 *
 | 
			
		||||
 * @param <T> type of stored object
 | 
			
		||||
 */
 | 
			
		||||
public class InMemoryDataStore<T> implements DataStore<T> {
 | 
			
		||||
 | 
			
		||||
  private final Map<String, T> store = new HashMap<>();
 | 
			
		||||
  private KeyGenerator generator = new UUIDKeyGenerator();
 | 
			
		||||
 | 
			
		||||
  @Override
 | 
			
		||||
  public String put(T item) {
 | 
			
		||||
    String key = generator.createKey();
 | 
			
		||||
    store.put(key, item);
 | 
			
		||||
    return key;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Override
 | 
			
		||||
  public void put(String id, T item) {
 | 
			
		||||
    store.put(id, item);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Override
 | 
			
		||||
  public Map<String, T> getAll() {
 | 
			
		||||
    return Collections.unmodifiableMap(store);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Override
 | 
			
		||||
  public void clear() {
 | 
			
		||||
    store.clear();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Override
 | 
			
		||||
  public void remove(String id) {
 | 
			
		||||
    store.remove(id);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Override
 | 
			
		||||
  public T get(String id) {
 | 
			
		||||
    return store.get(id);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,26 @@
 | 
			
		||||
package sonia.scm.store;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * In memory configuration store factory for testing purposes.
 | 
			
		||||
 *
 | 
			
		||||
 * @author Sebastian Sdorra
 | 
			
		||||
 */
 | 
			
		||||
public class InMemoryDataStoreFactory implements DataStoreFactory {
 | 
			
		||||
 | 
			
		||||
  private InMemoryDataStore store;
 | 
			
		||||
 | 
			
		||||
  public InMemoryDataStoreFactory() {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public InMemoryDataStoreFactory(InMemoryDataStore store) {
 | 
			
		||||
    this.store = store;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Override
 | 
			
		||||
  public <T> DataStore<T> getStore(TypedStoreParameters<T> storeParameters) {
 | 
			
		||||
    if (store != null) {
 | 
			
		||||
      return store;
 | 
			
		||||
    }
 | 
			
		||||
    return new InMemoryDataStore<>();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -2,6 +2,7 @@
 | 
			
		||||
import React from "react";
 | 
			
		||||
import { translate } from "react-i18next";
 | 
			
		||||
import Notification from "./Notification";
 | 
			
		||||
import {UNAUTHORIZED_ERROR} from "./apiclient";
 | 
			
		||||
 | 
			
		||||
type Props = {
 | 
			
		||||
  t: string => string,
 | 
			
		||||
@@ -9,14 +10,25 @@ type Props = {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class ErrorNotification extends React.Component<Props> {
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    const { t, error } = this.props;
 | 
			
		||||
    if (error) {
 | 
			
		||||
      return (
 | 
			
		||||
        <Notification type="danger">
 | 
			
		||||
          <strong>{t("error-notification.prefix")}:</strong> {error.message}
 | 
			
		||||
        </Notification>
 | 
			
		||||
      );
 | 
			
		||||
      if (error === UNAUTHORIZED_ERROR) {
 | 
			
		||||
        return (
 | 
			
		||||
          <Notification type="danger">
 | 
			
		||||
            <strong>{t("error-notification.prefix")}:</strong> {t("error-notification.timeout")}
 | 
			
		||||
            {" "}
 | 
			
		||||
            <a href="javascript:window.location.reload(true)">{t("error-notification.loginLink")}</a>
 | 
			
		||||
          </Notification>
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
        return (
 | 
			
		||||
          <Notification type="danger">
 | 
			
		||||
            <strong>{t("error-notification.prefix")}:</strong> {error.message}
 | 
			
		||||
          </Notification>
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -63,8 +63,9 @@ class ConfigurationBinder {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    // route for global configuration, passes the current repository to component
 | 
			
		||||
    const RepoRoute = ({ url, repository }) => {
 | 
			
		||||
      return this.route(url + to, <RepositoryComponent repository={repository}/>);
 | 
			
		||||
    const RepoRoute = ({url, repository}) => {
 | 
			
		||||
      const link = repository._links[linkName].href
 | 
			
		||||
      return this.route(url + to, <RepositoryComponent repository={repository} link={link}/>);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // bind config route to extension point
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,9 @@
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "error-notification": {
 | 
			
		||||
    "prefix": "Error"
 | 
			
		||||
    "prefix": "Error",
 | 
			
		||||
    "loginLink": "You can login here again.",
 | 
			
		||||
    "timeout": "The session has expired."
 | 
			
		||||
  },
 | 
			
		||||
  "loading": {
 | 
			
		||||
    "alt": "Loading ..."
 | 
			
		||||
 
 | 
			
		||||
@@ -32,9 +32,8 @@ export function fetchConfig(link: string) {
 | 
			
		||||
      .then(data => {
 | 
			
		||||
        dispatch(fetchConfigSuccess(data));
 | 
			
		||||
      })
 | 
			
		||||
      .catch(cause => {
 | 
			
		||||
        const error = new Error(`could not fetch config: ${cause.message}`);
 | 
			
		||||
        dispatch(fetchConfigFailure(error));
 | 
			
		||||
      .catch(err => {
 | 
			
		||||
        dispatch(fetchConfigFailure(err));
 | 
			
		||||
      });
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
@@ -73,13 +72,8 @@ export function modifyConfig(config: Config, callback?: () => void) {
 | 
			
		||||
          callback();
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      .catch(cause => {
 | 
			
		||||
        dispatch(
 | 
			
		||||
          modifyConfigFailure(
 | 
			
		||||
            config,
 | 
			
		||||
            new Error(`could not modify config: ${cause.message}`)
 | 
			
		||||
          )
 | 
			
		||||
        );
 | 
			
		||||
      .catch(err => {
 | 
			
		||||
        dispatch(modifyConfigFailure(config, err));
 | 
			
		||||
      });
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -54,9 +54,8 @@ export function fetchGroupsByLink(link: string) {
 | 
			
		||||
      .then(data => {
 | 
			
		||||
        dispatch(fetchGroupsSuccess(data));
 | 
			
		||||
      })
 | 
			
		||||
      .catch(cause => {
 | 
			
		||||
        const error = new Error(`could not fetch groups: ${cause.message}`);
 | 
			
		||||
        dispatch(fetchGroupsFailure(link, error));
 | 
			
		||||
      .catch(err => {
 | 
			
		||||
        dispatch(fetchGroupsFailure(link, err));
 | 
			
		||||
      });
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
@@ -105,9 +104,8 @@ function fetchGroup(link: string, name: string) {
 | 
			
		||||
      .then(data => {
 | 
			
		||||
        dispatch(fetchGroupSuccess(data));
 | 
			
		||||
      })
 | 
			
		||||
      .catch(cause => {
 | 
			
		||||
        const error = new Error(`could not fetch group: ${cause.message}`);
 | 
			
		||||
        dispatch(fetchGroupFailure(name, error));
 | 
			
		||||
      .catch(err => {
 | 
			
		||||
        dispatch(fetchGroupFailure(name, err));
 | 
			
		||||
      });
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
@@ -151,10 +149,10 @@ export function createGroup(link: string, group: Group, callback?: () => void) {
 | 
			
		||||
          callback();
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      .catch(error => {
 | 
			
		||||
      .catch(err => {
 | 
			
		||||
        dispatch(
 | 
			
		||||
          createGroupFailure(
 | 
			
		||||
            new Error(`Failed to create group ${group.name}: ${error.message}`)
 | 
			
		||||
            err
 | 
			
		||||
          )
 | 
			
		||||
        );
 | 
			
		||||
      });
 | 
			
		||||
@@ -201,11 +199,11 @@ export function modifyGroup(group: Group, callback?: () => void) {
 | 
			
		||||
      .then(() => {
 | 
			
		||||
        dispatch(fetchGroupByLink(group));
 | 
			
		||||
      })
 | 
			
		||||
      .catch(cause => {
 | 
			
		||||
      .catch(err => {
 | 
			
		||||
        dispatch(
 | 
			
		||||
          modifyGroupFailure(
 | 
			
		||||
            group,
 | 
			
		||||
            new Error(`could not modify group ${group.name}: ${cause.message}`)
 | 
			
		||||
            err
 | 
			
		||||
          )
 | 
			
		||||
        );
 | 
			
		||||
      });
 | 
			
		||||
@@ -259,11 +257,8 @@ export function deleteGroup(group: Group, callback?: () => void) {
 | 
			
		||||
          callback();
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      .catch(cause => {
 | 
			
		||||
        const error = new Error(
 | 
			
		||||
          `could not delete group ${group.name}: ${cause.message}`
 | 
			
		||||
        );
 | 
			
		||||
        dispatch(deleteGroupFailure(group, error));
 | 
			
		||||
      .catch(err => {
 | 
			
		||||
        dispatch(deleteGroupFailure(group, err));
 | 
			
		||||
      });
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -101,7 +101,7 @@ class RepositoryRoot extends React.Component<Props> {
 | 
			
		||||
    return (
 | 
			
		||||
      <Page title={repository.namespace + "/" + repository.name}>
 | 
			
		||||
        <div className="columns">
 | 
			
		||||
          <div className="column is-three-quarters">
 | 
			
		||||
          <div className="column is-three-quarters is-clipped">
 | 
			
		||||
            <Switch>
 | 
			
		||||
              <Route
 | 
			
		||||
                path={url}
 | 
			
		||||
 
 | 
			
		||||
@@ -224,9 +224,8 @@ export function modifyRepo(repository: Repository, callback?: () => void) {
 | 
			
		||||
      .then(() => {
 | 
			
		||||
        dispatch(fetchRepoByLink(repository));
 | 
			
		||||
      })
 | 
			
		||||
      .catch(cause => {
 | 
			
		||||
        const error = new Error(`failed to modify repo: ${cause.message}`);
 | 
			
		||||
        dispatch(modifyRepoFailure(repository, error));
 | 
			
		||||
      .catch(err => {
 | 
			
		||||
        dispatch(modifyRepoFailure(repository, err));
 | 
			
		||||
      });
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,16 @@
 | 
			
		||||
// @flow
 | 
			
		||||
 | 
			
		||||
import type {Action} from "@scm-manager/ui-components";
 | 
			
		||||
import {apiClient} from "@scm-manager/ui-components";
 | 
			
		||||
import type { Action } from "@scm-manager/ui-components";
 | 
			
		||||
import { apiClient } from "@scm-manager/ui-components";
 | 
			
		||||
import * as types from "../../../modules/types";
 | 
			
		||||
import type {Permission, PermissionCollection, PermissionCreateEntry} from "@scm-manager/ui-types";
 | 
			
		||||
import {isPending} from "../../../modules/pending";
 | 
			
		||||
import {getFailure} from "../../../modules/failure";
 | 
			
		||||
import {Dispatch} from "redux";
 | 
			
		||||
import type {
 | 
			
		||||
  Permission,
 | 
			
		||||
  PermissionCollection,
 | 
			
		||||
  PermissionCreateEntry
 | 
			
		||||
} from "@scm-manager/ui-types";
 | 
			
		||||
import { isPending } from "../../../modules/pending";
 | 
			
		||||
import { getFailure } from "../../../modules/failure";
 | 
			
		||||
import { Dispatch } from "redux";
 | 
			
		||||
 | 
			
		||||
export const FETCH_PERMISSIONS = "scm/permissions/FETCH_PERMISSIONS";
 | 
			
		||||
export const FETCH_PERMISSIONS_PENDING = `${FETCH_PERMISSIONS}_${
 | 
			
		||||
@@ -141,13 +145,8 @@ export function modifyPermission(
 | 
			
		||||
          callback();
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      .catch(cause => {
 | 
			
		||||
        const error = new Error(
 | 
			
		||||
          `failed to modify permission: ${cause.message}`
 | 
			
		||||
        );
 | 
			
		||||
        dispatch(
 | 
			
		||||
          modifyPermissionFailure(permission, error, namespace, repoName)
 | 
			
		||||
        );
 | 
			
		||||
      .catch(err => {
 | 
			
		||||
        dispatch(modifyPermissionFailure(permission, err, namespace, repoName));
 | 
			
		||||
      });
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
@@ -241,15 +240,7 @@ export function createPermission(
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      .catch(err =>
 | 
			
		||||
        dispatch(
 | 
			
		||||
          createPermissionFailure(
 | 
			
		||||
            new Error(
 | 
			
		||||
              `failed to add permission ${permission.name}: ${err.message}`
 | 
			
		||||
            ),
 | 
			
		||||
            namespace,
 | 
			
		||||
            repoName
 | 
			
		||||
          )
 | 
			
		||||
        )
 | 
			
		||||
        dispatch(createPermissionFailure(err, namespace, repoName))
 | 
			
		||||
      );
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
@@ -318,13 +309,8 @@ export function deletePermission(
 | 
			
		||||
          callback();
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      .catch(cause => {
 | 
			
		||||
        const error = new Error(
 | 
			
		||||
          `could not delete permission ${permission.name}: ${cause.message}`
 | 
			
		||||
        );
 | 
			
		||||
        dispatch(
 | 
			
		||||
          deletePermissionFailure(permission, namespace, repoName, error)
 | 
			
		||||
        );
 | 
			
		||||
      .catch(err => {
 | 
			
		||||
        dispatch(deletePermissionFailure(permission, namespace, repoName, err));
 | 
			
		||||
      });
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -119,7 +119,9 @@ class FileTree extends React.Component<Props> {
 | 
			
		||||
            <th className="is-hidden-mobile">
 | 
			
		||||
              {t("sources.file-tree.lastModified")}
 | 
			
		||||
            </th>
 | 
			
		||||
            <th>{t("sources.file-tree.description")}</th>
 | 
			
		||||
            <th className="is-hidden-mobile">
 | 
			
		||||
              {t("sources.file-tree.description")}
 | 
			
		||||
            </th>
 | 
			
		||||
          </tr>
 | 
			
		||||
        </thead>
 | 
			
		||||
        <tbody>
 | 
			
		||||
 
 | 
			
		||||
@@ -6,10 +6,14 @@ import FileSize from "./FileSize";
 | 
			
		||||
import FileIcon from "./FileIcon";
 | 
			
		||||
import { Link } from "react-router-dom";
 | 
			
		||||
import type { File } from "@scm-manager/ui-types";
 | 
			
		||||
import classNames from "classnames";
 | 
			
		||||
 | 
			
		||||
const styles = {
 | 
			
		||||
  iconColumn: {
 | 
			
		||||
    width: "16px"
 | 
			
		||||
  },
 | 
			
		||||
  wordBreakMinWidth: {
 | 
			
		||||
    minWidth: "10em"
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -71,12 +75,14 @@ class FileTreeLeaf extends React.Component<Props> {
 | 
			
		||||
    return (
 | 
			
		||||
      <tr>
 | 
			
		||||
        <td className={classes.iconColumn}>{this.createFileIcon(file)}</td>
 | 
			
		||||
        <td>{this.createFileName(file)}</td>
 | 
			
		||||
        <td className={classNames(classes.wordBreakMinWidth, "is-word-break")}>{this.createFileName(file)}</td>
 | 
			
		||||
        <td className="is-hidden-mobile">{fileSize}</td>
 | 
			
		||||
        <td className="is-hidden-mobile">
 | 
			
		||||
          <DateFromNow date={file.lastModified} />
 | 
			
		||||
        </td>
 | 
			
		||||
        <td>{file.description}</td>
 | 
			
		||||
        <td className={classNames(classes.wordBreakMinWidth, "is-word-break", "is-hidden-mobile")}>
 | 
			
		||||
          {file.description}
 | 
			
		||||
        </td>
 | 
			
		||||
      </tr>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -93,7 +93,7 @@ class Content extends React.Component<Props, State> {
 | 
			
		||||
                classes.marginInHeader
 | 
			
		||||
              )}
 | 
			
		||||
            />
 | 
			
		||||
            <span>{file.name}</span>
 | 
			
		||||
            <span className="is-word-break">{file.name}</span>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div className="media-right">{selector}</div>
 | 
			
		||||
        </article>
 | 
			
		||||
@@ -125,11 +125,11 @@ class Content extends React.Component<Props, State> {
 | 
			
		||||
            <tbody>
 | 
			
		||||
              <tr>
 | 
			
		||||
                <td>{t("sources.content.path")}</td>
 | 
			
		||||
                <td>{file.path}</td>
 | 
			
		||||
                <td className="is-word-break">{file.path}</td>
 | 
			
		||||
              </tr>
 | 
			
		||||
              <tr>
 | 
			
		||||
                <td>{t("sources.content.branch")}</td>
 | 
			
		||||
                <td>{revision}</td>
 | 
			
		||||
                <td className="is-word-break">{revision}</td>
 | 
			
		||||
              </tr>
 | 
			
		||||
              <tr>
 | 
			
		||||
                <td>{t("sources.content.size")}</td>
 | 
			
		||||
@@ -141,7 +141,7 @@ class Content extends React.Component<Props, State> {
 | 
			
		||||
              </tr>
 | 
			
		||||
              <tr>
 | 
			
		||||
                <td>{t("sources.content.description")}</td>
 | 
			
		||||
                <td>{description}</td>
 | 
			
		||||
                <td className="is-word-break">{description}</td>
 | 
			
		||||
              </tr>
 | 
			
		||||
            </tbody>
 | 
			
		||||
          </table>
 | 
			
		||||
 
 | 
			
		||||
@@ -25,8 +25,7 @@ export function fetchSources(
 | 
			
		||||
        dispatch(fetchSourcesSuccess(repository, revision, path, sources));
 | 
			
		||||
      })
 | 
			
		||||
      .catch(err => {
 | 
			
		||||
        const error = new Error(`failed to fetch sources: ${err.message}`);
 | 
			
		||||
        dispatch(fetchSourcesFailure(repository, revision, path, error));
 | 
			
		||||
        dispatch(fetchSourcesFailure(repository, revision, path, err));
 | 
			
		||||
      });
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -35,8 +35,6 @@ export const DELETE_USER_FAILURE = `${DELETE_USER}_${types.FAILURE_SUFFIX}`;
 | 
			
		||||
 | 
			
		||||
const CONTENT_TYPE_USER = "application/vnd.scmm-user+json;v=2";
 | 
			
		||||
 | 
			
		||||
// TODO i18n for error messages
 | 
			
		||||
 | 
			
		||||
// fetch users
 | 
			
		||||
 | 
			
		||||
export function fetchUsers(link: string) {
 | 
			
		||||
@@ -57,9 +55,8 @@ export function fetchUsersByLink(link: string) {
 | 
			
		||||
      .then(data => {
 | 
			
		||||
        dispatch(fetchUsersSuccess(data));
 | 
			
		||||
      })
 | 
			
		||||
      .catch(cause => {
 | 
			
		||||
        const error = new Error(`could not fetch users: ${cause.message}`);
 | 
			
		||||
        dispatch(fetchUsersFailure(link, error));
 | 
			
		||||
      .catch(err => {
 | 
			
		||||
        dispatch(fetchUsersFailure(link, err));
 | 
			
		||||
      });
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
@@ -108,9 +105,8 @@ function fetchUser(link: string, name: string) {
 | 
			
		||||
      .then(data => {
 | 
			
		||||
        dispatch(fetchUserSuccess(data));
 | 
			
		||||
      })
 | 
			
		||||
      .catch(cause => {
 | 
			
		||||
        const error = new Error(`could not fetch user: ${cause.message}`);
 | 
			
		||||
        dispatch(fetchUserFailure(name, error));
 | 
			
		||||
      .catch(err => {
 | 
			
		||||
        dispatch(fetchUserFailure(name, err));
 | 
			
		||||
      });
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
@@ -155,13 +151,7 @@ export function createUser(link: string, user: User, callback?: () => void) {
 | 
			
		||||
          callback();
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      .catch(err =>
 | 
			
		||||
        dispatch(
 | 
			
		||||
          createUserFailure(
 | 
			
		||||
            new Error(`failed to add user ${user.name}: ${err.message}`)
 | 
			
		||||
          )
 | 
			
		||||
        )
 | 
			
		||||
      );
 | 
			
		||||
      .catch(err => dispatch(createUserFailure(err)));
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -260,11 +250,8 @@ export function deleteUser(user: User, callback?: () => void) {
 | 
			
		||||
          callback();
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      .catch(cause => {
 | 
			
		||||
        const error = new Error(
 | 
			
		||||
          `could not delete user ${user.name}: ${cause.message}`
 | 
			
		||||
        );
 | 
			
		||||
        dispatch(deleteUserFailure(user, error));
 | 
			
		||||
      .catch(err => {
 | 
			
		||||
        dispatch(deleteUserFailure(user, err));
 | 
			
		||||
      });
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -27,6 +27,14 @@ $blue: #33B2E8;
 | 
			
		||||
  padding: 0 0 0 3.8em !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.is-word-break {
 | 
			
		||||
  -webkit-hyphens: auto;
 | 
			
		||||
  -moz-hyphens: auto;
 | 
			
		||||
  -ms-hyphens: auto;
 | 
			
		||||
  hyphens: auto;
 | 
			
		||||
  word-break: break-all;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.main {
 | 
			
		||||
  min-height: calc(100vh - 260px);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -79,14 +79,14 @@ import sonia.scm.repository.spi.HookEventFacade;
 | 
			
		||||
import sonia.scm.repository.xml.XmlRepositoryDAO;
 | 
			
		||||
import sonia.scm.schedule.QuartzScheduler;
 | 
			
		||||
import sonia.scm.schedule.Scheduler;
 | 
			
		||||
import sonia.scm.security.AccessTokenCookieIssuer;
 | 
			
		||||
import sonia.scm.security.AuthorizationChangedEventProducer;
 | 
			
		||||
import sonia.scm.security.CipherHandler;
 | 
			
		||||
import sonia.scm.security.CipherUtil;
 | 
			
		||||
import sonia.scm.security.ConfigurableLoginAttemptHandler;
 | 
			
		||||
import sonia.scm.security.DefaultJwtAccessTokenRefreshStrategy;
 | 
			
		||||
import sonia.scm.security.DefaultAccessTokenCookieIssuer;
 | 
			
		||||
import sonia.scm.security.DefaultKeyGenerator;
 | 
			
		||||
import sonia.scm.security.DefaultSecuritySystem;
 | 
			
		||||
import sonia.scm.security.JwtAccessTokenRefreshStrategy;
 | 
			
		||||
import sonia.scm.security.KeyGenerator;
 | 
			
		||||
import sonia.scm.security.LoginAttemptHandler;
 | 
			
		||||
import sonia.scm.security.SecuritySystem;
 | 
			
		||||
@@ -320,6 +320,7 @@ public class ScmServletModule extends ServletModule
 | 
			
		||||
    // bind events
 | 
			
		||||
    // bind(LastModifiedUpdateListener.class);
 | 
			
		||||
 | 
			
		||||
    bind(AccessTokenCookieIssuer.class).to(DefaultAccessTokenCookieIssuer.class);
 | 
			
		||||
    bind(PushStateDispatcher.class).toProvider(PushStateDispatcherProvider.class);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -51,12 +51,12 @@ import java.util.concurrent.TimeUnit;
 | 
			
		||||
 * @author Sebastian Sdorra
 | 
			
		||||
 * @since 2.0.0
 | 
			
		||||
 */
 | 
			
		||||
public final class AccessTokenCookieIssuer {
 | 
			
		||||
public final class DefaultAccessTokenCookieIssuer implements AccessTokenCookieIssuer {
 | 
			
		||||
  
 | 
			
		||||
  /**
 | 
			
		||||
   * the logger for AccessTokenCookieIssuer
 | 
			
		||||
   * the logger for DefaultAccessTokenCookieIssuer
 | 
			
		||||
   */
 | 
			
		||||
  private static final Logger LOG = LoggerFactory.getLogger(AccessTokenCookieIssuer.class);
 | 
			
		||||
  private static final Logger LOG = LoggerFactory.getLogger(DefaultAccessTokenCookieIssuer.class);
 | 
			
		||||
  
 | 
			
		||||
  private final ScmConfiguration configuration;
 | 
			
		||||
 | 
			
		||||
@@ -66,7 +66,7 @@ public final class AccessTokenCookieIssuer {
 | 
			
		||||
   * @param configuration scm main configuration
 | 
			
		||||
   */
 | 
			
		||||
  @Inject
 | 
			
		||||
  public AccessTokenCookieIssuer(ScmConfiguration configuration) {
 | 
			
		||||
  public DefaultAccessTokenCookieIssuer(ScmConfiguration configuration) {
 | 
			
		||||
    this.configuration = configuration;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
@@ -87,6 +87,7 @@ public final class JwtAccessToken implements AccessToken {
 | 
			
		||||
    return ofNullable(claims.get(REFRESHABLE_UNTIL_CLAIM_KEY, Date.class));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Override
 | 
			
		||||
  public Optional<String> getParentKey() {
 | 
			
		||||
    return ofNullable(claims.get(PARENT_TOKEN_ID_CLAIM_KEY).toString());
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,7 @@ import sonia.scm.security.AccessToken;
 | 
			
		||||
import sonia.scm.security.AccessTokenBuilder;
 | 
			
		||||
import sonia.scm.security.AccessTokenBuilderFactory;
 | 
			
		||||
import sonia.scm.security.AccessTokenCookieIssuer;
 | 
			
		||||
import sonia.scm.security.DefaultAccessTokenCookieIssuer;
 | 
			
		||||
 | 
			
		||||
import javax.servlet.http.HttpServletRequest;
 | 
			
		||||
import javax.servlet.http.HttpServletResponse;
 | 
			
		||||
@@ -46,7 +47,7 @@ public class AuthenticationResourceTest {
 | 
			
		||||
  @Mock
 | 
			
		||||
  private AccessTokenBuilder accessTokenBuilder;
 | 
			
		||||
 | 
			
		||||
  private AccessTokenCookieIssuer cookieIssuer = new AccessTokenCookieIssuer(mock(ScmConfiguration.class));
 | 
			
		||||
  private AccessTokenCookieIssuer cookieIssuer = new DefaultAccessTokenCookieIssuer(mock(ScmConfiguration.class));
 | 
			
		||||
 | 
			
		||||
  private static final String AUTH_JSON_TRILLIAN = "{\n" +
 | 
			
		||||
    "\t\"cookie\": true,\n" +
 | 
			
		||||
 
 | 
			
		||||
@@ -20,11 +20,11 @@ import static org.mockito.Mockito.verify;
 | 
			
		||||
import static org.mockito.Mockito.when;
 | 
			
		||||
 | 
			
		||||
@RunWith(MockitoJUnitRunner.class)
 | 
			
		||||
public class AccessTokenCookieIssuerTest {
 | 
			
		||||
public class DefaultAccessTokenCookieIssuerTest {
 | 
			
		||||
 | 
			
		||||
  private ScmConfiguration configuration;
 | 
			
		||||
 | 
			
		||||
  private AccessTokenCookieIssuer issuer;
 | 
			
		||||
  private DefaultAccessTokenCookieIssuer issuer;
 | 
			
		||||
 | 
			
		||||
  @Mock
 | 
			
		||||
  private HttpServletRequest request;
 | 
			
		||||
@@ -41,7 +41,7 @@ public class AccessTokenCookieIssuerTest {
 | 
			
		||||
  @Before
 | 
			
		||||
  public void setUp() {
 | 
			
		||||
    configuration = new ScmConfiguration();
 | 
			
		||||
    issuer = new AccessTokenCookieIssuer(configuration);
 | 
			
		||||
    issuer = new DefaultAccessTokenCookieIssuer(configuration);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
		Reference in New Issue
	
	Block a user