mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-01 11:05:56 +01:00
Merge with 2.0.0-m3
This commit is contained in:
@@ -80,8 +80,20 @@ public interface AccessToken {
|
|||||||
*/
|
*/
|
||||||
Date getExpiration();
|
Date getExpiration();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns refresh expiration of token.
|
||||||
|
*
|
||||||
|
* @return refresh expiration
|
||||||
|
*/
|
||||||
Optional<Date> getRefreshExpiration();
|
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
|
* 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
|
* 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;
|
String result = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
byte[] encodedInput = Base64.getDecoder().decode(value);
|
byte[] encodedInput = Base64.getUrlDecoder().decode(value);
|
||||||
byte[] salt = new byte[SALT_LENGTH];
|
byte[] salt = new byte[SALT_LENGTH];
|
||||||
byte[] encoded = new byte[encodedInput.length - 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(salt, 0, result, 0, SALT_LENGTH);
|
||||||
System.arraycopy(encodedInput, 0, result, SALT_LENGTH,
|
System.arraycopy(encodedInput, 0, result, SALT_LENGTH,
|
||||||
result.length - 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) {
|
} catch (IOException | GeneralSecurityException ex) {
|
||||||
throw new CipherException("could not encode string", 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 --------------------------------------------------------
|
//~--- non-JDK imports --------------------------------------------------------
|
||||||
|
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
|
import org.junit.Before;
|
||||||
import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider;
|
import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider;
|
||||||
import sonia.scm.repository.GitRepositoryConfig;
|
import sonia.scm.repository.GitRepositoryConfig;
|
||||||
|
import sonia.scm.store.InMemoryConfigurationStore;
|
||||||
import sonia.scm.store.InMemoryConfigurationStoreFactory;
|
import sonia.scm.store.InMemoryConfigurationStoreFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -69,7 +71,7 @@ public class AbstractGitCommandTestBase extends ZippedRepositoryTestBase
|
|||||||
{
|
{
|
||||||
if (context == null)
|
if (context == null)
|
||||||
{
|
{
|
||||||
context = new GitContext(repositoryDirectory, repository, new GitRepositoryConfigStoreProvider(new InMemoryConfigurationStoreFactory()));
|
context = new GitContext(repositoryDirectory, repository, new GitRepositoryConfigStoreProvider(InMemoryConfigurationStoreFactory.create()));
|
||||||
}
|
}
|
||||||
|
|
||||||
return context;
|
return context;
|
||||||
|
|||||||
@@ -35,55 +35,33 @@ package sonia.scm.store;
|
|||||||
|
|
||||||
//~--- non-JDK imports --------------------------------------------------------
|
//~--- non-JDK imports --------------------------------------------------------
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* In memory configuration store factory for testing purposes.
|
* 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
|
* @author Sebastian Sdorra
|
||||||
*/
|
*/
|
||||||
public class InMemoryConfigurationStoreFactory implements ConfigurationStoreFactory {
|
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
|
@Override
|
||||||
public ConfigurationStore getStore(TypedStoreParameters storeParameters) {
|
public ConfigurationStore getStore(TypedStoreParameters storeParameters) {
|
||||||
Key key = new Key(storeParameters.getType(), storeParameters.getName(), storeParameters.getRepository() == null? "-": storeParameters.getRepository().getId());
|
if (store != null) {
|
||||||
if (STORES.containsKey(key)) {
|
|
||||||
return STORES.get(key);
|
|
||||||
} else {
|
|
||||||
InMemoryConfigurationStore<Object> store = new InMemoryConfigurationStore<>();
|
|
||||||
STORES.put(key, store);
|
|
||||||
return store;
|
return store;
|
||||||
}
|
}
|
||||||
}
|
return new InMemoryConfigurationStore<>();
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 React from "react";
|
||||||
import { translate } from "react-i18next";
|
import { translate } from "react-i18next";
|
||||||
import Notification from "./Notification";
|
import Notification from "./Notification";
|
||||||
|
import {UNAUTHORIZED_ERROR} from "./apiclient";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
t: string => string,
|
t: string => string,
|
||||||
@@ -9,14 +10,25 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
class ErrorNotification extends React.Component<Props> {
|
class ErrorNotification extends React.Component<Props> {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { t, error } = this.props;
|
const { t, error } = this.props;
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
if (error === UNAUTHORIZED_ERROR) {
|
||||||
<Notification type="danger">
|
return (
|
||||||
<strong>{t("error-notification.prefix")}:</strong> {error.message}
|
<Notification type="danger">
|
||||||
</Notification>
|
<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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,8 +63,9 @@ class ConfigurationBinder {
|
|||||||
|
|
||||||
|
|
||||||
// route for global configuration, passes the current repository to component
|
// route for global configuration, passes the current repository to component
|
||||||
const RepoRoute = ({ url, repository }) => {
|
const RepoRoute = ({url, repository}) => {
|
||||||
return this.route(url + to, <RepositoryComponent repository={repository}/>);
|
const link = repository._links[linkName].href
|
||||||
|
return this.route(url + to, <RepositoryComponent repository={repository} link={link}/>);
|
||||||
};
|
};
|
||||||
|
|
||||||
// bind config route to extension point
|
// bind config route to extension point
|
||||||
|
|||||||
@@ -20,7 +20,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"error-notification": {
|
"error-notification": {
|
||||||
"prefix": "Error"
|
"prefix": "Error",
|
||||||
|
"loginLink": "You can login here again.",
|
||||||
|
"timeout": "The session has expired."
|
||||||
},
|
},
|
||||||
"loading": {
|
"loading": {
|
||||||
"alt": "Loading ..."
|
"alt": "Loading ..."
|
||||||
|
|||||||
@@ -32,9 +32,8 @@ export function fetchConfig(link: string) {
|
|||||||
.then(data => {
|
.then(data => {
|
||||||
dispatch(fetchConfigSuccess(data));
|
dispatch(fetchConfigSuccess(data));
|
||||||
})
|
})
|
||||||
.catch(cause => {
|
.catch(err => {
|
||||||
const error = new Error(`could not fetch config: ${cause.message}`);
|
dispatch(fetchConfigFailure(err));
|
||||||
dispatch(fetchConfigFailure(error));
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -73,13 +72,8 @@ export function modifyConfig(config: Config, callback?: () => void) {
|
|||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(cause => {
|
.catch(err => {
|
||||||
dispatch(
|
dispatch(modifyConfigFailure(config, err));
|
||||||
modifyConfigFailure(
|
|
||||||
config,
|
|
||||||
new Error(`could not modify config: ${cause.message}`)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,9 +54,8 @@ export function fetchGroupsByLink(link: string) {
|
|||||||
.then(data => {
|
.then(data => {
|
||||||
dispatch(fetchGroupsSuccess(data));
|
dispatch(fetchGroupsSuccess(data));
|
||||||
})
|
})
|
||||||
.catch(cause => {
|
.catch(err => {
|
||||||
const error = new Error(`could not fetch groups: ${cause.message}`);
|
dispatch(fetchGroupsFailure(link, err));
|
||||||
dispatch(fetchGroupsFailure(link, error));
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -105,9 +104,8 @@ function fetchGroup(link: string, name: string) {
|
|||||||
.then(data => {
|
.then(data => {
|
||||||
dispatch(fetchGroupSuccess(data));
|
dispatch(fetchGroupSuccess(data));
|
||||||
})
|
})
|
||||||
.catch(cause => {
|
.catch(err => {
|
||||||
const error = new Error(`could not fetch group: ${cause.message}`);
|
dispatch(fetchGroupFailure(name, err));
|
||||||
dispatch(fetchGroupFailure(name, error));
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -151,10 +149,10 @@ export function createGroup(link: string, group: Group, callback?: () => void) {
|
|||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(err => {
|
||||||
dispatch(
|
dispatch(
|
||||||
createGroupFailure(
|
createGroupFailure(
|
||||||
new Error(`Failed to create group ${group.name}: ${error.message}`)
|
err
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -201,11 +199,11 @@ export function modifyGroup(group: Group, callback?: () => void) {
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
dispatch(fetchGroupByLink(group));
|
dispatch(fetchGroupByLink(group));
|
||||||
})
|
})
|
||||||
.catch(cause => {
|
.catch(err => {
|
||||||
dispatch(
|
dispatch(
|
||||||
modifyGroupFailure(
|
modifyGroupFailure(
|
||||||
group,
|
group,
|
||||||
new Error(`could not modify group ${group.name}: ${cause.message}`)
|
err
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -259,11 +257,8 @@ export function deleteGroup(group: Group, callback?: () => void) {
|
|||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(cause => {
|
.catch(err => {
|
||||||
const error = new Error(
|
dispatch(deleteGroupFailure(group, err));
|
||||||
`could not delete group ${group.name}: ${cause.message}`
|
|
||||||
);
|
|
||||||
dispatch(deleteGroupFailure(group, error));
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ class RepositoryRoot extends React.Component<Props> {
|
|||||||
return (
|
return (
|
||||||
<Page title={repository.namespace + "/" + repository.name}>
|
<Page title={repository.namespace + "/" + repository.name}>
|
||||||
<div className="columns">
|
<div className="columns">
|
||||||
<div className="column is-three-quarters">
|
<div className="column is-three-quarters is-clipped">
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route
|
<Route
|
||||||
path={url}
|
path={url}
|
||||||
|
|||||||
@@ -224,9 +224,8 @@ export function modifyRepo(repository: Repository, callback?: () => void) {
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
dispatch(fetchRepoByLink(repository));
|
dispatch(fetchRepoByLink(repository));
|
||||||
})
|
})
|
||||||
.catch(cause => {
|
.catch(err => {
|
||||||
const error = new Error(`failed to modify repo: ${cause.message}`);
|
dispatch(modifyRepoFailure(repository, err));
|
||||||
dispatch(modifyRepoFailure(repository, error));
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import type {Action} from "@scm-manager/ui-components";
|
import type { Action } from "@scm-manager/ui-components";
|
||||||
import {apiClient} from "@scm-manager/ui-components";
|
import { apiClient } from "@scm-manager/ui-components";
|
||||||
import * as types from "../../../modules/types";
|
import * as types from "../../../modules/types";
|
||||||
import type {Permission, PermissionCollection, PermissionCreateEntry} from "@scm-manager/ui-types";
|
import type {
|
||||||
import {isPending} from "../../../modules/pending";
|
Permission,
|
||||||
import {getFailure} from "../../../modules/failure";
|
PermissionCollection,
|
||||||
import {Dispatch} from "redux";
|
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 = "scm/permissions/FETCH_PERMISSIONS";
|
||||||
export const FETCH_PERMISSIONS_PENDING = `${FETCH_PERMISSIONS}_${
|
export const FETCH_PERMISSIONS_PENDING = `${FETCH_PERMISSIONS}_${
|
||||||
@@ -141,13 +145,8 @@ export function modifyPermission(
|
|||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(cause => {
|
.catch(err => {
|
||||||
const error = new Error(
|
dispatch(modifyPermissionFailure(permission, err, namespace, repoName));
|
||||||
`failed to modify permission: ${cause.message}`
|
|
||||||
);
|
|
||||||
dispatch(
|
|
||||||
modifyPermissionFailure(permission, error, namespace, repoName)
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -241,15 +240,7 @@ export function createPermission(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err =>
|
.catch(err =>
|
||||||
dispatch(
|
dispatch(createPermissionFailure(err, namespace, repoName))
|
||||||
createPermissionFailure(
|
|
||||||
new Error(
|
|
||||||
`failed to add permission ${permission.name}: ${err.message}`
|
|
||||||
),
|
|
||||||
namespace,
|
|
||||||
repoName
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -318,13 +309,8 @@ export function deletePermission(
|
|||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(cause => {
|
.catch(err => {
|
||||||
const error = new Error(
|
dispatch(deletePermissionFailure(permission, namespace, repoName, err));
|
||||||
`could not delete permission ${permission.name}: ${cause.message}`
|
|
||||||
);
|
|
||||||
dispatch(
|
|
||||||
deletePermissionFailure(permission, namespace, repoName, error)
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,7 +119,9 @@ class FileTree extends React.Component<Props> {
|
|||||||
<th className="is-hidden-mobile">
|
<th className="is-hidden-mobile">
|
||||||
{t("sources.file-tree.lastModified")}
|
{t("sources.file-tree.lastModified")}
|
||||||
</th>
|
</th>
|
||||||
<th>{t("sources.file-tree.description")}</th>
|
<th className="is-hidden-mobile">
|
||||||
|
{t("sources.file-tree.description")}
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|||||||
@@ -6,10 +6,14 @@ import FileSize from "./FileSize";
|
|||||||
import FileIcon from "./FileIcon";
|
import FileIcon from "./FileIcon";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import type { File } from "@scm-manager/ui-types";
|
import type { File } from "@scm-manager/ui-types";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
iconColumn: {
|
iconColumn: {
|
||||||
width: "16px"
|
width: "16px"
|
||||||
|
},
|
||||||
|
wordBreakMinWidth: {
|
||||||
|
minWidth: "10em"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -71,12 +75,14 @@ class FileTreeLeaf extends React.Component<Props> {
|
|||||||
return (
|
return (
|
||||||
<tr>
|
<tr>
|
||||||
<td className={classes.iconColumn}>{this.createFileIcon(file)}</td>
|
<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">{fileSize}</td>
|
||||||
<td className="is-hidden-mobile">
|
<td className="is-hidden-mobile">
|
||||||
<DateFromNow date={file.lastModified} />
|
<DateFromNow date={file.lastModified} />
|
||||||
</td>
|
</td>
|
||||||
<td>{file.description}</td>
|
<td className={classNames(classes.wordBreakMinWidth, "is-word-break", "is-hidden-mobile")}>
|
||||||
|
{file.description}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ class Content extends React.Component<Props, State> {
|
|||||||
classes.marginInHeader
|
classes.marginInHeader
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<span>{file.name}</span>
|
<span className="is-word-break">{file.name}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="media-right">{selector}</div>
|
<div className="media-right">{selector}</div>
|
||||||
</article>
|
</article>
|
||||||
@@ -125,11 +125,11 @@ class Content extends React.Component<Props, State> {
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{t("sources.content.path")}</td>
|
<td>{t("sources.content.path")}</td>
|
||||||
<td>{file.path}</td>
|
<td className="is-word-break">{file.path}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{t("sources.content.branch")}</td>
|
<td>{t("sources.content.branch")}</td>
|
||||||
<td>{revision}</td>
|
<td className="is-word-break">{revision}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{t("sources.content.size")}</td>
|
<td>{t("sources.content.size")}</td>
|
||||||
@@ -141,7 +141,7 @@ class Content extends React.Component<Props, State> {
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{t("sources.content.description")}</td>
|
<td>{t("sources.content.description")}</td>
|
||||||
<td>{description}</td>
|
<td className="is-word-break">{description}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -25,8 +25,7 @@ export function fetchSources(
|
|||||||
dispatch(fetchSourcesSuccess(repository, revision, path, sources));
|
dispatch(fetchSourcesSuccess(repository, revision, path, sources));
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
const error = new Error(`failed to fetch sources: ${err.message}`);
|
dispatch(fetchSourcesFailure(repository, revision, path, err));
|
||||||
dispatch(fetchSourcesFailure(repository, revision, path, error));
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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";
|
const CONTENT_TYPE_USER = "application/vnd.scmm-user+json;v=2";
|
||||||
|
|
||||||
// TODO i18n for error messages
|
|
||||||
|
|
||||||
// fetch users
|
// fetch users
|
||||||
|
|
||||||
export function fetchUsers(link: string) {
|
export function fetchUsers(link: string) {
|
||||||
@@ -57,9 +55,8 @@ export function fetchUsersByLink(link: string) {
|
|||||||
.then(data => {
|
.then(data => {
|
||||||
dispatch(fetchUsersSuccess(data));
|
dispatch(fetchUsersSuccess(data));
|
||||||
})
|
})
|
||||||
.catch(cause => {
|
.catch(err => {
|
||||||
const error = new Error(`could not fetch users: ${cause.message}`);
|
dispatch(fetchUsersFailure(link, err));
|
||||||
dispatch(fetchUsersFailure(link, error));
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -108,9 +105,8 @@ function fetchUser(link: string, name: string) {
|
|||||||
.then(data => {
|
.then(data => {
|
||||||
dispatch(fetchUserSuccess(data));
|
dispatch(fetchUserSuccess(data));
|
||||||
})
|
})
|
||||||
.catch(cause => {
|
.catch(err => {
|
||||||
const error = new Error(`could not fetch user: ${cause.message}`);
|
dispatch(fetchUserFailure(name, err));
|
||||||
dispatch(fetchUserFailure(name, error));
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -155,13 +151,7 @@ export function createUser(link: string, user: User, callback?: () => void) {
|
|||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err =>
|
.catch(err => dispatch(createUserFailure(err)));
|
||||||
dispatch(
|
|
||||||
createUserFailure(
|
|
||||||
new Error(`failed to add user ${user.name}: ${err.message}`)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,11 +250,8 @@ export function deleteUser(user: User, callback?: () => void) {
|
|||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(cause => {
|
.catch(err => {
|
||||||
const error = new Error(
|
dispatch(deleteUserFailure(user, err));
|
||||||
`could not delete user ${user.name}: ${cause.message}`
|
|
||||||
);
|
|
||||||
dispatch(deleteUserFailure(user, error));
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,14 @@ $blue: #33B2E8;
|
|||||||
padding: 0 0 0 3.8em !important;
|
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 {
|
.main {
|
||||||
min-height: calc(100vh - 260px);
|
min-height: calc(100vh - 260px);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,14 +79,14 @@ import sonia.scm.repository.spi.HookEventFacade;
|
|||||||
import sonia.scm.repository.xml.XmlRepositoryDAO;
|
import sonia.scm.repository.xml.XmlRepositoryDAO;
|
||||||
import sonia.scm.schedule.QuartzScheduler;
|
import sonia.scm.schedule.QuartzScheduler;
|
||||||
import sonia.scm.schedule.Scheduler;
|
import sonia.scm.schedule.Scheduler;
|
||||||
|
import sonia.scm.security.AccessTokenCookieIssuer;
|
||||||
import sonia.scm.security.AuthorizationChangedEventProducer;
|
import sonia.scm.security.AuthorizationChangedEventProducer;
|
||||||
import sonia.scm.security.CipherHandler;
|
import sonia.scm.security.CipherHandler;
|
||||||
import sonia.scm.security.CipherUtil;
|
import sonia.scm.security.CipherUtil;
|
||||||
import sonia.scm.security.ConfigurableLoginAttemptHandler;
|
import sonia.scm.security.ConfigurableLoginAttemptHandler;
|
||||||
import sonia.scm.security.DefaultJwtAccessTokenRefreshStrategy;
|
import sonia.scm.security.DefaultAccessTokenCookieIssuer;
|
||||||
import sonia.scm.security.DefaultKeyGenerator;
|
import sonia.scm.security.DefaultKeyGenerator;
|
||||||
import sonia.scm.security.DefaultSecuritySystem;
|
import sonia.scm.security.DefaultSecuritySystem;
|
||||||
import sonia.scm.security.JwtAccessTokenRefreshStrategy;
|
|
||||||
import sonia.scm.security.KeyGenerator;
|
import sonia.scm.security.KeyGenerator;
|
||||||
import sonia.scm.security.LoginAttemptHandler;
|
import sonia.scm.security.LoginAttemptHandler;
|
||||||
import sonia.scm.security.SecuritySystem;
|
import sonia.scm.security.SecuritySystem;
|
||||||
@@ -320,6 +320,7 @@ public class ScmServletModule extends ServletModule
|
|||||||
// bind events
|
// bind events
|
||||||
// bind(LastModifiedUpdateListener.class);
|
// bind(LastModifiedUpdateListener.class);
|
||||||
|
|
||||||
|
bind(AccessTokenCookieIssuer.class).to(DefaultAccessTokenCookieIssuer.class);
|
||||||
bind(PushStateDispatcher.class).toProvider(PushStateDispatcherProvider.class);
|
bind(PushStateDispatcher.class).toProvider(PushStateDispatcherProvider.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,12 +51,12 @@ import java.util.concurrent.TimeUnit;
|
|||||||
* @author Sebastian Sdorra
|
* @author Sebastian Sdorra
|
||||||
* @since 2.0.0
|
* @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;
|
private final ScmConfiguration configuration;
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ public final class AccessTokenCookieIssuer {
|
|||||||
* @param configuration scm main configuration
|
* @param configuration scm main configuration
|
||||||
*/
|
*/
|
||||||
@Inject
|
@Inject
|
||||||
public AccessTokenCookieIssuer(ScmConfiguration configuration) {
|
public DefaultAccessTokenCookieIssuer(ScmConfiguration configuration) {
|
||||||
this.configuration = configuration;
|
this.configuration = configuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,6 +87,7 @@ public final class JwtAccessToken implements AccessToken {
|
|||||||
return ofNullable(claims.get(REFRESHABLE_UNTIL_CLAIM_KEY, Date.class));
|
return ofNullable(claims.get(REFRESHABLE_UNTIL_CLAIM_KEY, Date.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public Optional<String> getParentKey() {
|
public Optional<String> getParentKey() {
|
||||||
return ofNullable(claims.get(PARENT_TOKEN_ID_CLAIM_KEY).toString());
|
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.AccessTokenBuilder;
|
||||||
import sonia.scm.security.AccessTokenBuilderFactory;
|
import sonia.scm.security.AccessTokenBuilderFactory;
|
||||||
import sonia.scm.security.AccessTokenCookieIssuer;
|
import sonia.scm.security.AccessTokenCookieIssuer;
|
||||||
|
import sonia.scm.security.DefaultAccessTokenCookieIssuer;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
@@ -46,7 +47,7 @@ public class AuthenticationResourceTest {
|
|||||||
@Mock
|
@Mock
|
||||||
private AccessTokenBuilder accessTokenBuilder;
|
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" +
|
private static final String AUTH_JSON_TRILLIAN = "{\n" +
|
||||||
"\t\"cookie\": true,\n" +
|
"\t\"cookie\": true,\n" +
|
||||||
|
|||||||
@@ -20,11 +20,11 @@ import static org.mockito.Mockito.verify;
|
|||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
@RunWith(MockitoJUnitRunner.class)
|
@RunWith(MockitoJUnitRunner.class)
|
||||||
public class AccessTokenCookieIssuerTest {
|
public class DefaultAccessTokenCookieIssuerTest {
|
||||||
|
|
||||||
private ScmConfiguration configuration;
|
private ScmConfiguration configuration;
|
||||||
|
|
||||||
private AccessTokenCookieIssuer issuer;
|
private DefaultAccessTokenCookieIssuer issuer;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private HttpServletRequest request;
|
private HttpServletRequest request;
|
||||||
@@ -41,7 +41,7 @@ public class AccessTokenCookieIssuerTest {
|
|||||||
@Before
|
@Before
|
||||||
public void setUp() {
|
public void setUp() {
|
||||||
configuration = new ScmConfiguration();
|
configuration = new ScmConfiguration();
|
||||||
issuer = new AccessTokenCookieIssuer(configuration);
|
issuer = new DefaultAccessTokenCookieIssuer(configuration);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
Reference in New Issue
Block a user