mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-09 06:55:47 +01:00
add anonymous mode for webclient / change footer and redirects if user is anonymous / add login button if user is anonymous
This commit is contained in:
@@ -29,6 +29,8 @@ import sonia.scm.SCMContext;
|
|||||||
|
|
||||||
public class Authentications {
|
public class Authentications {
|
||||||
|
|
||||||
|
private Authentications() {}
|
||||||
|
|
||||||
public static boolean isAuthenticatedSubjectAnonymous() {
|
public static boolean isAuthenticatedSubjectAnonymous() {
|
||||||
return isSubjectAnonymous((String) SecurityUtils.getSubject().getPrincipal());
|
return isSubjectAnonymous((String) SecurityUtils.getSubject().getPrincipal());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package sonia.scm.web.filter;
|
package sonia.scm.web.filter;
|
||||||
|
|
||||||
//~--- non-JDK imports --------------------------------------------------------
|
//~--- non-JDK imports --------------------------------------------------------
|
||||||
@@ -59,16 +59,21 @@ import java.util.Set;
|
|||||||
* @since 2.0.0
|
* @since 2.0.0
|
||||||
*/
|
*/
|
||||||
@Singleton
|
@Singleton
|
||||||
public class AuthenticationFilter extends HttpFilter
|
public class AuthenticationFilter extends HttpFilter {
|
||||||
{
|
|
||||||
|
|
||||||
/** marker for failed authentication */
|
/**
|
||||||
|
* marker for failed authentication
|
||||||
|
*/
|
||||||
private static final String ATTRIBUTE_FAILED_AUTH = "sonia.scm.auth.failed";
|
private static final String ATTRIBUTE_FAILED_AUTH = "sonia.scm.auth.failed";
|
||||||
|
|
||||||
/** Field description */
|
/**
|
||||||
|
* Field description
|
||||||
|
*/
|
||||||
private static final String HEADER_AUTHORIZATION = "Authorization";
|
private static final String HEADER_AUTHORIZATION = "Authorization";
|
||||||
|
|
||||||
/** the logger for AuthenticationFilter */
|
/**
|
||||||
|
* the logger for AuthenticationFilter
|
||||||
|
*/
|
||||||
private static final Logger logger =
|
private static final Logger logger =
|
||||||
LoggerFactory.getLogger(AuthenticationFilter.class);
|
LoggerFactory.getLogger(AuthenticationFilter.class);
|
||||||
|
|
||||||
@@ -77,7 +82,7 @@ public class AuthenticationFilter extends HttpFilter
|
|||||||
/**
|
/**
|
||||||
* Constructs a new basic authenticaton filter.
|
* Constructs a new basic authenticaton filter.
|
||||||
*
|
*
|
||||||
* @param configuration scm-manager global configuration
|
* @param configuration scm-manager global configuration
|
||||||
* @param tokenGenerators web token generators
|
* @param tokenGenerators web token generators
|
||||||
*/
|
*/
|
||||||
@Inject
|
@Inject
|
||||||
@@ -92,41 +97,32 @@ public class AuthenticationFilter extends HttpFilter
|
|||||||
* Handles authentication, if a one of the {@link WebTokenGenerator} returns
|
* Handles authentication, if a one of the {@link WebTokenGenerator} returns
|
||||||
* an {@link AuthenticationToken}.
|
* an {@link AuthenticationToken}.
|
||||||
*
|
*
|
||||||
* @param request servlet request
|
* @param request servlet request
|
||||||
* @param response servlet response
|
* @param response servlet response
|
||||||
* @param chain filter chain
|
* @param chain filter chain
|
||||||
*
|
|
||||||
* @throws IOException
|
* @throws IOException
|
||||||
* @throws ServletException
|
* @throws ServletException
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
protected void doFilter(HttpServletRequest request,
|
protected void doFilter(HttpServletRequest request,
|
||||||
HttpServletResponse response, FilterChain chain)
|
HttpServletResponse response, FilterChain chain)
|
||||||
throws IOException, ServletException
|
throws IOException, ServletException {
|
||||||
{
|
|
||||||
Subject subject = SecurityUtils.getSubject();
|
Subject subject = SecurityUtils.getSubject();
|
||||||
|
|
||||||
AuthenticationToken token = createToken(request);
|
AuthenticationToken token = createToken(request);
|
||||||
|
|
||||||
if (token != null)
|
if (token != null) {
|
||||||
{
|
|
||||||
logger.trace(
|
logger.trace(
|
||||||
"found authentication token on request, start authentication");
|
"found authentication token on request, start authentication");
|
||||||
handleAuthentication(request, response, chain, subject, token);
|
handleAuthentication(request, response, chain, subject, token);
|
||||||
}
|
} else if (subject.isAuthenticated()) {
|
||||||
else if (subject.isAuthenticated())
|
|
||||||
{
|
|
||||||
logger.trace("user is already authenticated");
|
logger.trace("user is already authenticated");
|
||||||
processChain(request, response, chain, subject);
|
processChain(request, response, chain, subject);
|
||||||
}
|
} else if (isAnonymousAccessEnabled()) {
|
||||||
else if (isAnonymousAccessEnabled() && !HttpUtil.isWUIRequest(request))
|
|
||||||
{
|
|
||||||
logger.trace("anonymous access granted");
|
logger.trace("anonymous access granted");
|
||||||
subject.login(new AnonymousToken());
|
subject.login(new AnonymousToken());
|
||||||
processChain(request, response, chain, subject);
|
processChain(request, response, chain, subject);
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
logger.trace("could not find user send unauthorized");
|
logger.trace("could not find user send unauthorized");
|
||||||
handleUnauthorized(request, response, chain);
|
handleUnauthorized(request, response, chain);
|
||||||
}
|
}
|
||||||
@@ -136,28 +132,22 @@ public class AuthenticationFilter extends HttpFilter
|
|||||||
* Sends status code 403 back to client, if the authentication has failed.
|
* Sends status code 403 back to client, if the authentication has failed.
|
||||||
* In all other cases the method will send status code 403 back to client.
|
* In all other cases the method will send status code 403 back to client.
|
||||||
*
|
*
|
||||||
* @param request servlet request
|
* @param request servlet request
|
||||||
* @param response servlet response
|
* @param response servlet response
|
||||||
* @param chain filter chain
|
* @param chain filter chain
|
||||||
*
|
|
||||||
* @throws IOException
|
* @throws IOException
|
||||||
* @throws ServletException
|
* @throws ServletException
|
||||||
*
|
|
||||||
* @since 1.8
|
* @since 1.8
|
||||||
*/
|
*/
|
||||||
protected void handleUnauthorized(HttpServletRequest request,
|
protected void handleUnauthorized(HttpServletRequest request,
|
||||||
HttpServletResponse response, FilterChain chain)
|
HttpServletResponse response, FilterChain chain)
|
||||||
throws IOException, ServletException
|
throws IOException, ServletException {
|
||||||
{
|
|
||||||
|
|
||||||
// send only forbidden, if the authentication has failed.
|
// send only forbidden, if the authentication has failed.
|
||||||
// see https://bitbucket.org/sdorra/scm-manager/issue/545/git-clone-with-username-in-url-does-not
|
// see https://bitbucket.org/sdorra/scm-manager/issue/545/git-clone-with-username-in-url-does-not
|
||||||
if (Boolean.TRUE.equals(request.getAttribute(ATTRIBUTE_FAILED_AUTH)))
|
if (Boolean.TRUE.equals(request.getAttribute(ATTRIBUTE_FAILED_AUTH))) {
|
||||||
{
|
|
||||||
sendFailedAuthenticationError(request, response);
|
sendFailedAuthenticationError(request, response);
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
sendUnauthorizedError(request, response);
|
sendUnauthorizedError(request, response);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,16 +155,13 @@ public class AuthenticationFilter extends HttpFilter
|
|||||||
/**
|
/**
|
||||||
* Sends an error for a failed authentication back to client.
|
* Sends an error for a failed authentication back to client.
|
||||||
*
|
*
|
||||||
*
|
* @param request http request
|
||||||
* @param request http request
|
|
||||||
* @param response http response
|
* @param response http response
|
||||||
*
|
|
||||||
* @throws IOException
|
* @throws IOException
|
||||||
*/
|
*/
|
||||||
protected void sendFailedAuthenticationError(HttpServletRequest request,
|
protected void sendFailedAuthenticationError(HttpServletRequest request,
|
||||||
HttpServletResponse response)
|
HttpServletResponse response)
|
||||||
throws IOException
|
throws IOException {
|
||||||
{
|
|
||||||
HttpUtil.sendUnauthorized(request, response,
|
HttpUtil.sendUnauthorized(request, response,
|
||||||
configuration.getRealmDescription());
|
configuration.getRealmDescription());
|
||||||
}
|
}
|
||||||
@@ -182,16 +169,13 @@ public class AuthenticationFilter extends HttpFilter
|
|||||||
/**
|
/**
|
||||||
* Sends an unauthorized error back to client.
|
* Sends an unauthorized error back to client.
|
||||||
*
|
*
|
||||||
*
|
* @param request http request
|
||||||
* @param request http request
|
|
||||||
* @param response http response
|
* @param response http response
|
||||||
*
|
|
||||||
* @throws IOException
|
* @throws IOException
|
||||||
*/
|
*/
|
||||||
protected void sendUnauthorizedError(HttpServletRequest request,
|
protected void sendUnauthorizedError(HttpServletRequest request,
|
||||||
HttpServletResponse response)
|
HttpServletResponse response)
|
||||||
throws IOException
|
throws IOException {
|
||||||
{
|
|
||||||
HttpUtil.sendUnauthorized(request, response,
|
HttpUtil.sendUnauthorized(request, response,
|
||||||
configuration.getRealmDescription());
|
configuration.getRealmDescription());
|
||||||
}
|
}
|
||||||
@@ -200,20 +184,15 @@ public class AuthenticationFilter extends HttpFilter
|
|||||||
* Iterates all {@link WebTokenGenerator} and creates an
|
* Iterates all {@link WebTokenGenerator} and creates an
|
||||||
* {@link AuthenticationToken} from the given request.
|
* {@link AuthenticationToken} from the given request.
|
||||||
*
|
*
|
||||||
*
|
|
||||||
* @param request http servlet request
|
* @param request http servlet request
|
||||||
*
|
|
||||||
* @return authentication token of {@code null}
|
* @return authentication token of {@code null}
|
||||||
*/
|
*/
|
||||||
private AuthenticationToken createToken(HttpServletRequest request)
|
private AuthenticationToken createToken(HttpServletRequest request) {
|
||||||
{
|
|
||||||
AuthenticationToken token = null;
|
AuthenticationToken token = null;
|
||||||
for (WebTokenGenerator generator : tokenGenerators)
|
for (WebTokenGenerator generator : tokenGenerators) {
|
||||||
{
|
|
||||||
token = generator.createToken(request);
|
token = generator.createToken(request);
|
||||||
|
|
||||||
if (token != null)
|
if (token != null) {
|
||||||
{
|
|
||||||
logger.trace("generated web token {} from generator {}",
|
logger.trace("generated web token {} from generator {}",
|
||||||
token.getClass(), generator.getClass());
|
token.getClass(), generator.getClass());
|
||||||
|
|
||||||
@@ -227,30 +206,24 @@ public class AuthenticationFilter extends HttpFilter
|
|||||||
/**
|
/**
|
||||||
* Handle authentication with the given {@link AuthenticationToken}.
|
* Handle authentication with the given {@link AuthenticationToken}.
|
||||||
*
|
*
|
||||||
*
|
* @param request http servlet request
|
||||||
* @param request http servlet request
|
|
||||||
* @param response http servlet response
|
* @param response http servlet response
|
||||||
* @param chain filter chain
|
* @param chain filter chain
|
||||||
* @param subject subject
|
* @param subject subject
|
||||||
* @param token authentication token
|
* @param token authentication token
|
||||||
*
|
|
||||||
* @throws IOException
|
* @throws IOException
|
||||||
* @throws ServletException
|
* @throws ServletException
|
||||||
*/
|
*/
|
||||||
private void handleAuthentication(HttpServletRequest request,
|
private void handleAuthentication(HttpServletRequest request,
|
||||||
HttpServletResponse response, FilterChain chain, Subject subject,
|
HttpServletResponse response, FilterChain chain, Subject subject,
|
||||||
AuthenticationToken token)
|
AuthenticationToken token)
|
||||||
throws IOException, ServletException
|
throws IOException, ServletException {
|
||||||
{
|
|
||||||
logger.trace("found basic authorization header, start authentication");
|
logger.trace("found basic authorization header, start authentication");
|
||||||
|
|
||||||
try
|
try {
|
||||||
{
|
|
||||||
subject.login(token);
|
subject.login(token);
|
||||||
processChain(request, response, chain, subject);
|
processChain(request, response, chain, subject);
|
||||||
}
|
} catch (AuthenticationException ex) {
|
||||||
catch (AuthenticationException ex)
|
|
||||||
{
|
|
||||||
logger.warn("authentication failed", ex);
|
logger.warn("authentication failed", ex);
|
||||||
handleUnauthorized(request, response, chain);
|
handleUnauthorized(request, response, chain);
|
||||||
}
|
}
|
||||||
@@ -259,33 +232,26 @@ public class AuthenticationFilter extends HttpFilter
|
|||||||
/**
|
/**
|
||||||
* Process the filter chain.
|
* Process the filter chain.
|
||||||
*
|
*
|
||||||
*
|
* @param request http servlet request
|
||||||
* @param request http servlet request
|
|
||||||
* @param response http servlet response
|
* @param response http servlet response
|
||||||
* @param chain filter chain
|
* @param chain filter chain
|
||||||
* @param subject subject
|
* @param subject subject
|
||||||
*
|
|
||||||
* @throws IOException
|
* @throws IOException
|
||||||
* @throws ServletException
|
* @throws ServletException
|
||||||
*/
|
*/
|
||||||
private void processChain(HttpServletRequest request,
|
private void processChain(HttpServletRequest request,
|
||||||
HttpServletResponse response, FilterChain chain, Subject subject)
|
HttpServletResponse response, FilterChain chain, Subject subject)
|
||||||
throws IOException, ServletException
|
throws IOException, ServletException {
|
||||||
{
|
|
||||||
String username = Util.EMPTY_STRING;
|
String username = Util.EMPTY_STRING;
|
||||||
|
|
||||||
if (!subject.isAuthenticated())
|
if (!subject.isAuthenticated()) {
|
||||||
{
|
|
||||||
|
|
||||||
// anonymous access
|
// anonymous access
|
||||||
username = SCMContext.USER_ANONYMOUS;
|
username = SCMContext.USER_ANONYMOUS;
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
Object obj = subject.getPrincipal();
|
Object obj = subject.getPrincipal();
|
||||||
|
|
||||||
if (obj != null)
|
if (obj != null) {
|
||||||
{
|
|
||||||
username = obj.toString();
|
username = obj.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -299,19 +265,21 @@ public class AuthenticationFilter extends HttpFilter
|
|||||||
/**
|
/**
|
||||||
* Returns {@code true} if anonymous access is enabled.
|
* Returns {@code true} if anonymous access is enabled.
|
||||||
*
|
*
|
||||||
*
|
|
||||||
* @return {@code true} if anonymous access is enabled
|
* @return {@code true} if anonymous access is enabled
|
||||||
*/
|
*/
|
||||||
private boolean isAnonymousAccessEnabled()
|
private boolean isAnonymousAccessEnabled() {
|
||||||
{
|
|
||||||
return (configuration != null) && configuration.getAnonymousMode() != AnonymousMode.OFF;
|
return (configuration != null) && configuration.getAnonymousMode() != AnonymousMode.OFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
//~--- fields ---------------------------------------------------------------
|
//~--- fields ---------------------------------------------------------------
|
||||||
|
|
||||||
/** set of web token generators */
|
/**
|
||||||
|
* set of web token generators
|
||||||
|
*/
|
||||||
private final Set<WebTokenGenerator> tokenGenerators;
|
private final Set<WebTokenGenerator> tokenGenerators;
|
||||||
|
|
||||||
/** scm main configuration */
|
/**
|
||||||
|
* scm main configuration
|
||||||
|
*/
|
||||||
protected ScmConfiguration configuration;
|
protected ScmConfiguration configuration;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@scm-manager/ui-extensions": "^2.1.0",
|
"@scm-manager/ui-extensions": "^2.1.0",
|
||||||
"@scm-manager/ui-types": "^2.3.0",
|
"@scm-manager/ui-types": "^2.4.0-SNAPSHOT",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"date-fns": "^2.4.1",
|
"date-fns": "^2.4.1",
|
||||||
"gitdiff-parser": "^0.1.2",
|
"gitdiff-parser": "^0.1.2",
|
||||||
|
|||||||
@@ -89,14 +89,24 @@ const Footer: FC<Props> = ({ me, version, links }) => {
|
|||||||
meSectionTile = <TitleWithIcon title={me.displayName} icon="user-circle" />;
|
meSectionTile = <TitleWithIcon title={me.displayName} icon="user-circle" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let meSectionBody = <div />;
|
||||||
|
{
|
||||||
|
if (me.name !== "_anonymous")
|
||||||
|
meSectionBody = (
|
||||||
|
<>
|
||||||
|
<NavLink to="/me/settings/password" label={t("profile.changePasswordNavLink")} />
|
||||||
|
<ExtensionPoint name="profile.setting" props={extensionProps} renderAll={true} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="footer">
|
<footer className="footer">
|
||||||
<section className="section container">
|
<section className="section container">
|
||||||
<div className="columns is-size-7">
|
<div className="columns is-size-7">
|
||||||
<FooterSection title={meSectionTile}>
|
<FooterSection title={meSectionTile}>
|
||||||
<NavLink to="/me" label={t("footer.user.profile")} />
|
<NavLink to="/me" label={t("footer.user.profile")} />
|
||||||
<NavLink to="/me/settings/password" label={t("profile.changePasswordNavLink")} />
|
{meSectionBody}
|
||||||
<ExtensionPoint name="profile.setting" props={extensionProps} renderAll={true} />
|
|
||||||
</FooterSection>
|
</FooterSection>
|
||||||
<FooterSection title={<TitleWithIcon title={t("footer.information.title")} icon="info-circle" />}>
|
<FooterSection title={<TitleWithIcon title={t("footer.information.title")} icon="info-circle" />}>
|
||||||
<ExternalNavLink to="https://www.scm-manager.org/" label={`SCM-Manager ${version}`} />
|
<ExternalNavLink to="https://www.scm-manager.org/" label={`SCM-Manager ${version}`} />
|
||||||
|
|||||||
@@ -63,6 +63,23 @@ class PrimaryNavigation extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
appendLogin = (navigationItems: ReactNode[], append: Appender) => {
|
||||||
|
const { t, links } = this.props;
|
||||||
|
|
||||||
|
const props = {
|
||||||
|
links,
|
||||||
|
label: t("primary-navigation.login")
|
||||||
|
};
|
||||||
|
|
||||||
|
if (binder.hasExtension("primary-navigation.login", props)) {
|
||||||
|
navigationItems.push(
|
||||||
|
<ExtensionPoint key="primary-navigation.login" name="primary-navigation.login" props={props} />
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
append("/login", "/login", "primary-navigation.login", "login");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
createNavigationItems = () => {
|
createNavigationItems = () => {
|
||||||
const navigationItems: ReactNode[] = [];
|
const navigationItems: ReactNode[] = [];
|
||||||
const { t, links } = this.props;
|
const { t, links } = this.props;
|
||||||
@@ -95,6 +112,7 @@ class PrimaryNavigation extends React.Component<Props> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
this.appendLogout(navigationItems, append);
|
this.appendLogout(navigationItems, append);
|
||||||
|
this.appendLogin(navigationItems, append);
|
||||||
|
|
||||||
return navigationItems;
|
return navigationItems;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
"@scm-manager/tsconfig": "^2.1.0",
|
"@scm-manager/tsconfig": "^2.1.0",
|
||||||
"@scm-manager/ui-scripts": "^2.1.0",
|
"@scm-manager/ui-scripts": "^2.1.0",
|
||||||
"@scm-manager/ui-tests": "^2.1.0",
|
"@scm-manager/ui-tests": "^2.1.0",
|
||||||
"@scm-manager/ui-types": "^2.3.0",
|
"@scm-manager/ui-types": "^2.4.0-SNAPSHOT",
|
||||||
"@types/classnames": "^2.2.9",
|
"@types/classnames": "^2.2.9",
|
||||||
"@types/enzyme": "^3.10.3",
|
"@types/enzyme": "^3.10.3",
|
||||||
"@types/fetch-mock": "^7.3.1",
|
"@types/fetch-mock": "^7.3.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@scm-manager/ui-types",
|
"name": "@scm-manager/ui-types",
|
||||||
"version": "2.3.0",
|
"version": "2.4.0-SNAPSHOT",
|
||||||
"description": "Flow types for SCM-Manager related Objects",
|
"description": "Flow types for SCM-Manager related Objects",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"files": [
|
"files": [
|
||||||
|
|||||||
@@ -48,6 +48,7 @@
|
|||||||
"repositories": "Repositories",
|
"repositories": "Repositories",
|
||||||
"users": "Benutzer",
|
"users": "Benutzer",
|
||||||
"logout": "Abmelden",
|
"logout": "Abmelden",
|
||||||
|
"login": "Anmelden",
|
||||||
"groups": "Gruppen",
|
"groups": "Gruppen",
|
||||||
"admin": "Administration"
|
"admin": "Administration"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -49,6 +49,7 @@
|
|||||||
"repositories": "Repositories",
|
"repositories": "Repositories",
|
||||||
"users": "Users",
|
"users": "Users",
|
||||||
"logout": "Logout",
|
"logout": "Logout",
|
||||||
|
"login": "Login",
|
||||||
"groups": "Groups",
|
"groups": "Groups",
|
||||||
"admin": "Administration"
|
"admin": "Administration"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import { connect } from "react-redux";
|
|||||||
import { Redirect, withRouter } from "react-router-dom";
|
import { Redirect, withRouter } from "react-router-dom";
|
||||||
import { compose } from "redux";
|
import { compose } from "redux";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { getLoginFailure, isAuthenticated, isLoginPending, login } from "../modules/auth";
|
import { getLoginFailure, isAnonymous, isLoginPending, login } from "../modules/auth";
|
||||||
import { getLoginInfoLink, getLoginLink } from "../modules/indexResource";
|
import { getLoginInfoLink, getLoginLink } from "../modules/indexResource";
|
||||||
import LoginInfo from "../components/LoginInfo";
|
import LoginInfo from "../components/LoginInfo";
|
||||||
|
|
||||||
@@ -86,7 +86,7 @@ class Login extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = (state: any) => {
|
const mapStateToProps = (state: any) => {
|
||||||
const authenticated = isAuthenticated(state);
|
const authenticated = state?.auth?.me && !isAnonymous(state.auth.me);
|
||||||
const loading = isLoginPending(state);
|
const loading = isLoginPending(state);
|
||||||
const error = getLoginFailure(state);
|
const error = getLoginFailure(state);
|
||||||
const link = getLoginLink(state);
|
const link = getLoginLink(state);
|
||||||
|
|||||||
@@ -43,7 +43,9 @@ type Props = WithTranslation & {
|
|||||||
|
|
||||||
class Logout extends React.Component<Props> {
|
class Logout extends React.Component<Props> {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.logout(this.props.logoutLink);
|
if (this.props.logoutLink) {
|
||||||
|
this.props.logout(this.props.logoutLink);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
*/
|
*/
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Route, RouteComponentProps, withRouter } from "react-router-dom";
|
import { Route, RouteComponentProps, withRouter } from "react-router-dom";
|
||||||
import { getMe } from "../modules/auth";
|
import { getMe, isAnonymous } from "../modules/auth";
|
||||||
import { compose } from "redux";
|
import { compose } from "redux";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { WithTranslation, withTranslation } from "react-i18next";
|
import { WithTranslation, withTranslation } from "react-i18next";
|
||||||
@@ -92,7 +92,9 @@ class Profile extends React.Component<Props> {
|
|||||||
<CustomQueryFlexWrappedColumns>
|
<CustomQueryFlexWrappedColumns>
|
||||||
<PrimaryContentColumn>
|
<PrimaryContentColumn>
|
||||||
<Route path={url} exact render={() => <ProfileInfo me={me} />} />
|
<Route path={url} exact render={() => <ProfileInfo me={me} />} />
|
||||||
<Route path={`${url}/settings/password`} render={() => <ChangeUserPassword me={me} />} />
|
{me?._links?.password && (
|
||||||
|
<Route path={`${url}/settings/password`} render={() => <ChangeUserPassword me={me} />} />
|
||||||
|
)}
|
||||||
<ExtensionPoint name="profile.route" props={extensionProps} renderAll={true} />
|
<ExtensionPoint name="profile.route" props={extensionProps} renderAll={true} />
|
||||||
</PrimaryContentColumn>
|
</PrimaryContentColumn>
|
||||||
<SecondaryNavigationColumn>
|
<SecondaryNavigationColumn>
|
||||||
@@ -103,14 +105,16 @@ class Profile extends React.Component<Props> {
|
|||||||
label={t("profile.informationNavLink")}
|
label={t("profile.informationNavLink")}
|
||||||
title={t("profile.informationNavLink")}
|
title={t("profile.informationNavLink")}
|
||||||
/>
|
/>
|
||||||
<SubNavigation
|
{!isAnonymous(me) && (
|
||||||
to={`${url}/settings/password`}
|
<SubNavigation
|
||||||
label={t("profile.settingsNavLink")}
|
to={`${url}/settings/password`}
|
||||||
title={t("profile.settingsNavLink")}
|
label={t("profile.settingsNavLink")}
|
||||||
>
|
title={t("profile.settingsNavLink")}
|
||||||
<NavLink to={`${url}/settings/password`} label={t("profile.changePasswordNavLink")} />
|
>
|
||||||
<ExtensionPoint name="profile.setting" props={extensionProps} renderAll={true} />
|
<NavLink to={`${url}/settings/password`} label={t("profile.changePasswordNavLink")} />
|
||||||
</SubNavigation>
|
<ExtensionPoint name="profile.setting" props={extensionProps} renderAll={true} />
|
||||||
|
</SubNavigation>
|
||||||
|
)}
|
||||||
</SecondaryNavigation>
|
</SecondaryNavigation>
|
||||||
</SecondaryNavigationColumn>
|
</SecondaryNavigationColumn>
|
||||||
</CustomQueryFlexWrappedColumns>
|
</CustomQueryFlexWrappedColumns>
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import reducer, {
|
|||||||
getLoginFailure,
|
getLoginFailure,
|
||||||
getLogoutFailure,
|
getLogoutFailure,
|
||||||
getMe,
|
getMe,
|
||||||
|
isAnonymous,
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
isFetchMePending,
|
isFetchMePending,
|
||||||
isLoginPending,
|
isLoginPending,
|
||||||
@@ -117,7 +118,7 @@ describe("auth actions", () => {
|
|||||||
fetchMock.postOnce("/api/v2/auth/access_token", {
|
fetchMock.postOnce("/api/v2/auth/access_token", {
|
||||||
body: {
|
body: {
|
||||||
cookie: true,
|
cookie: true,
|
||||||
grant_type: "password",
|
grantType: "password",
|
||||||
username: "tricia",
|
username: "tricia",
|
||||||
password: "secret123"
|
password: "secret123"
|
||||||
},
|
},
|
||||||
@@ -349,23 +350,6 @@ describe("auth selectors", () => {
|
|||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return false if me exist and login Link does exist", () => {
|
|
||||||
expect(
|
|
||||||
isAuthenticated({
|
|
||||||
auth: {
|
|
||||||
me
|
|
||||||
},
|
|
||||||
indexResources: {
|
|
||||||
links: {
|
|
||||||
login: {
|
|
||||||
href: "login.href"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return me", () => {
|
it("should return me", () => {
|
||||||
expect(
|
expect(
|
||||||
getMe({
|
getMe({
|
||||||
@@ -500,3 +484,8 @@ describe("auth selectors", () => {
|
|||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should check if current user is anonymous", () => {
|
||||||
|
expect(isAnonymous({ name: "_anonymous", displayName: "Anon", _links: [], groups: [], mail: "" })).toBeTruthy();
|
||||||
|
expect(isAnonymous({ name: "scmadmin", displayName: "SCM Admin", _links: [], groups: [], mail: "" })).toBeFalsy();
|
||||||
|
});
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Me } from "@scm-manager/ui-types";
|
import { Link, Me } from "@scm-manager/ui-types";
|
||||||
import * as types from "./types";
|
import * as types from "./types";
|
||||||
|
|
||||||
import { apiClient, UnauthorizedError } from "@scm-manager/ui-components";
|
import { apiClient, UnauthorizedError } from "@scm-manager/ui-components";
|
||||||
@@ -32,9 +32,9 @@ import {
|
|||||||
callFetchIndexResources,
|
callFetchIndexResources,
|
||||||
fetchIndexResources,
|
fetchIndexResources,
|
||||||
fetchIndexResourcesPending,
|
fetchIndexResourcesPending,
|
||||||
fetchIndexResourcesSuccess,
|
fetchIndexResourcesSuccess, getLoginLink
|
||||||
getLoginLink
|
|
||||||
} from "./indexResource";
|
} from "./indexResource";
|
||||||
|
import { AnyAction } from "redux";
|
||||||
|
|
||||||
// Action
|
// Action
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ const initialState = {};
|
|||||||
|
|
||||||
export default function reducer(
|
export default function reducer(
|
||||||
state: object = initialState,
|
state: object = initialState,
|
||||||
action: object = {
|
action: AnyAction = {
|
||||||
type: "UNKNOWN"
|
type: "UNKNOWN"
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
@@ -174,23 +174,23 @@ const callFetchMe = (link: string): Promise<Me> => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const login = (loginLink: string, username: string, password: string) => {
|
export const login = (loginLink: string, username: string, password: string) => {
|
||||||
const login_data = {
|
const loginData = {
|
||||||
cookie: true,
|
cookie: true,
|
||||||
grant_type: "password",
|
grantType: "password",
|
||||||
username,
|
username,
|
||||||
password
|
password
|
||||||
};
|
};
|
||||||
return function(dispatch: any) {
|
return function(dispatch: any) {
|
||||||
dispatch(loginPending());
|
dispatch(loginPending());
|
||||||
return apiClient
|
return apiClient
|
||||||
.post(loginLink, login_data)
|
.post(loginLink, loginData)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
dispatch(fetchIndexResourcesPending());
|
dispatch(fetchIndexResourcesPending());
|
||||||
return callFetchIndexResources();
|
return callFetchIndexResources();
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
dispatch(fetchIndexResourcesSuccess(response));
|
dispatch(fetchIndexResourcesSuccess(response));
|
||||||
const meLink = response._links.me.href;
|
const meLink = (response._links.me as Link).href;
|
||||||
return callFetchMe(meLink);
|
return callFetchMe(meLink);
|
||||||
})
|
})
|
||||||
.then(me => {
|
.then(me => {
|
||||||
@@ -256,17 +256,17 @@ export const logout = (link: string) => {
|
|||||||
// selectors
|
// selectors
|
||||||
|
|
||||||
const stateAuth = (state: object): object => {
|
const stateAuth = (state: object): object => {
|
||||||
|
// @ts-ignore Right types for redux not available
|
||||||
return state.auth || {};
|
return state.auth || {};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isAuthenticated = (state: object) => {
|
export const isAuthenticated = (state: object) => {
|
||||||
if (state.auth.me && !getLoginLink(state)) {
|
// @ts-ignore Right types for redux not available
|
||||||
return true;
|
return !!((state.auth.me && !getLoginLink(state)) || isAnonymous(state.auth.me));
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getMe = (state: object): Me => {
|
export const getMe = (state: object): Me => {
|
||||||
|
// @ts-ignore Right types for redux not available
|
||||||
return stateAuth(state).me;
|
return stateAuth(state).me;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -295,5 +295,12 @@ export const getLogoutFailure = (state: object) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const isRedirecting = (state: object) => {
|
export const isRedirecting = (state: object) => {
|
||||||
|
// @ts-ignore Right types for redux not available
|
||||||
return !!stateAuth(state).redirecting;
|
return !!stateAuth(state).redirecting;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Helper methods
|
||||||
|
|
||||||
|
export const isAnonymous = (me: Me) => {
|
||||||
|
return me?.name === "_anonymous";
|
||||||
|
};
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ function removeAllEntriesOfIdentifierFromState(state: object, payload: any, iden
|
|||||||
const newState = {};
|
const newState = {};
|
||||||
for (const failureType in state) {
|
for (const failureType in state) {
|
||||||
if (failureType !== identifier && !failureType.startsWith(identifier)) {
|
if (failureType !== identifier && !failureType.startsWith(identifier)) {
|
||||||
|
// @ts-ignore Right types not available
|
||||||
newState[failureType] = state[failureType];
|
newState[failureType] = state[failureType];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -50,6 +51,7 @@ function removeFromState(state: object, identifier: string) {
|
|||||||
const newState = {};
|
const newState = {};
|
||||||
for (const failureType in state) {
|
for (const failureType in state) {
|
||||||
if (failureType !== identifier) {
|
if (failureType !== identifier) {
|
||||||
|
// @ts-ignore Right types not available
|
||||||
newState[failureType] = state[failureType];
|
newState[failureType] = state[failureType];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,11 +92,13 @@ export default function reducer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getFailure(state: object, actionType: string, itemId?: string | number) {
|
export function getFailure(state: object, actionType: string, itemId?: string | number) {
|
||||||
|
// @ts-ignore Right types not available
|
||||||
if (state.failure) {
|
if (state.failure) {
|
||||||
let identifier = actionType;
|
let identifier = actionType;
|
||||||
if (itemId) {
|
if (itemId) {
|
||||||
identifier += "/" + itemId;
|
identifier += "/" + itemId;
|
||||||
}
|
}
|
||||||
|
// @ts-ignore Right types not available
|
||||||
return state.failure[identifier];
|
return state.failure[identifier];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,23 +111,29 @@ export function getFetchIndexResourcesFailure(state: object) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getLinks(state: object) {
|
export function getLinks(state: object) {
|
||||||
|
// @ts-ignore Right types not available
|
||||||
return state.indexResources.links;
|
return state.indexResources.links;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getLink(state: object, name: string) {
|
export function getLink(state: object, name: string) {
|
||||||
|
// @ts-ignore Right types not available
|
||||||
if (state.indexResources.links && state.indexResources.links[name]) {
|
if (state.indexResources.links && state.indexResources.links[name]) {
|
||||||
|
// @ts-ignore Right types not available
|
||||||
return state.indexResources.links[name].href;
|
return state.indexResources.links[name].href;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getLinkCollection(state: object, name: string): Link[] {
|
export function getLinkCollection(state: object, name: string): Link[] {
|
||||||
|
// @ts-ignore Right types not available
|
||||||
if (state.indexResources.links && state.indexResources.links[name]) {
|
if (state.indexResources.links && state.indexResources.links[name]) {
|
||||||
|
// @ts-ignore Right types not available
|
||||||
return state.indexResources.links[name];
|
return state.indexResources.links[name];
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAppVersion(state: object) {
|
export function getAppVersion(state: object) {
|
||||||
|
// @ts-ignore Right types not available
|
||||||
return state.indexResources.version;
|
return state.indexResources.version;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ function removeFromState(state: object, identifier: string) {
|
|||||||
const newState = {};
|
const newState = {};
|
||||||
for (const childType in state) {
|
for (const childType in state) {
|
||||||
if (childType !== identifier) {
|
if (childType !== identifier) {
|
||||||
|
// @ts-ignore Right types not available
|
||||||
newState[childType] = state[childType];
|
newState[childType] = state[childType];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -42,6 +43,7 @@ function removeAllEntriesOfIdentifierFromState(state: object, payload: any, iden
|
|||||||
const newState = {};
|
const newState = {};
|
||||||
for (const childType in state) {
|
for (const childType in state) {
|
||||||
if (childType !== identifier && !childType.startsWith(identifier)) {
|
if (childType !== identifier && !childType.startsWith(identifier)) {
|
||||||
|
// @ts-ignore Right types not available
|
||||||
newState[childType] = state[childType];
|
newState[childType] = state[childType];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,6 +94,7 @@ export function isPending(state: object, actionType: string, itemId?: string | n
|
|||||||
if (itemId) {
|
if (itemId) {
|
||||||
type += "/" + itemId;
|
type += "/" + itemId;
|
||||||
}
|
}
|
||||||
|
// @ts-ignore Right types not available
|
||||||
if (state.pending && state.pending[type]) {
|
if (state.pending && state.pending[type]) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package sonia.scm.api.v2.resources;
|
package sonia.scm.api.v2.resources;
|
||||||
|
|
||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
@@ -35,6 +35,7 @@ import sonia.scm.config.ConfigurationPermissions;
|
|||||||
import sonia.scm.config.ScmConfiguration;
|
import sonia.scm.config.ScmConfiguration;
|
||||||
import sonia.scm.group.GroupPermissions;
|
import sonia.scm.group.GroupPermissions;
|
||||||
import sonia.scm.plugin.PluginPermissions;
|
import sonia.scm.plugin.PluginPermissions;
|
||||||
|
import sonia.scm.security.AnonymousMode;
|
||||||
import sonia.scm.security.Authentications;
|
import sonia.scm.security.Authentications;
|
||||||
import sonia.scm.security.PermissionPermissions;
|
import sonia.scm.security.PermissionPermissions;
|
||||||
import sonia.scm.user.UserPermissions;
|
import sonia.scm.user.UserPermissions;
|
||||||
@@ -70,7 +71,7 @@ public class IndexDtoGenerator extends HalAppenderMapper {
|
|||||||
builder.single(link("loginInfo", loginInfoUrl));
|
builder.single(link("loginInfo", loginInfoUrl));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (SecurityUtils.getSubject().isAuthenticated()) {
|
if (SecurityUtils.getSubject().isAuthenticated() && !Authentications.isAuthenticatedSubjectAnonymous() || isAnonymousAccess()) {
|
||||||
builder.single(link("me", resourceLinks.me().self()));
|
builder.single(link("me", resourceLinks.me().self()));
|
||||||
|
|
||||||
if (Authentications.isAuthenticatedSubjectAnonymous()) {
|
if (Authentications.isAuthenticatedSubjectAnonymous()) {
|
||||||
@@ -120,4 +121,8 @@ public class IndexDtoGenerator extends HalAppenderMapper {
|
|||||||
|
|
||||||
return new IndexDto(builder.build(), embeddedBuilder.build(), scmContextProvider.getVersion());
|
return new IndexDto(builder.build(), embeddedBuilder.build(), scmContextProvider.getVersion());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isAnonymousAccess() {
|
||||||
|
return Authentications.isAuthenticatedSubjectAnonymous() && configuration.getAnonymousMode() == AnonymousMode.FULL;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,14 +83,16 @@ public class MeDtoFactory extends HalAppenderMapper {
|
|||||||
|
|
||||||
private MeDto createDto(User user) {
|
private MeDto createDto(User user) {
|
||||||
Links.Builder linksBuilder = linkingTo().self(resourceLinks.me().self());
|
Links.Builder linksBuilder = linkingTo().self(resourceLinks.me().self());
|
||||||
if (UserPermissions.delete(user).isPermitted()) {
|
if (isNotAnonymous(user)) {
|
||||||
linksBuilder.single(link("delete", resourceLinks.me().delete(user.getName())));
|
if (UserPermissions.delete(user).isPermitted()) {
|
||||||
}
|
linksBuilder.single(link("delete", resourceLinks.me().delete(user.getName())));
|
||||||
if (UserPermissions.modify(user).isPermitted()) {
|
}
|
||||||
linksBuilder.single(link("update", resourceLinks.me().update(user.getName())));
|
if (UserPermissions.modify(user).isPermitted()) {
|
||||||
}
|
linksBuilder.single(link("update", resourceLinks.me().update(user.getName())));
|
||||||
if (userManager.isTypeDefault(user) && UserPermissions.changePassword(user).isPermitted() && !Authentications.isSubjectAnonymous(user.getName())) {
|
}
|
||||||
linksBuilder.single(link("password", resourceLinks.me().passwordChange()));
|
if (userManager.isTypeDefault(user) && UserPermissions.changePassword(user).isPermitted()) {
|
||||||
|
linksBuilder.single(link("password", resourceLinks.me().passwordChange()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Embedded.Builder embeddedBuilder = embeddedBuilder();
|
Embedded.Builder embeddedBuilder = embeddedBuilder();
|
||||||
@@ -99,4 +101,7 @@ public class MeDtoFactory extends HalAppenderMapper {
|
|||||||
return new MeDto(linksBuilder.build(), embeddedBuilder.build());
|
return new MeDto(linksBuilder.build(), embeddedBuilder.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isNotAnonymous(User user) {
|
||||||
|
return !Authentications.isSubjectAnonymous(user.getName());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.api.v2.resources;
|
||||||
|
|
||||||
|
import org.apache.shiro.subject.Subject;
|
||||||
|
import org.apache.shiro.util.ThreadContext;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import sonia.scm.BasicContextProvider;
|
||||||
|
import sonia.scm.config.ScmConfiguration;
|
||||||
|
import sonia.scm.security.AnonymousMode;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
import static sonia.scm.SCMContext.USER_ANONYMOUS;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class IndexDtoGeneratorTest {
|
||||||
|
|
||||||
|
private static final ScmPathInfo scmPathInfo = () -> URI.create("/api/v2");
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ScmConfiguration configuration;
|
||||||
|
@Mock
|
||||||
|
private BasicContextProvider contextProvider;
|
||||||
|
@Mock
|
||||||
|
private ResourceLinks resourceLinks;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private Subject subject;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private IndexDtoGenerator generator;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void bindSubject() {
|
||||||
|
ThreadContext.bind(subject);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void tearDownSubject() {
|
||||||
|
ThreadContext.unbindSubject();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldAppendMeIfAuthenticated() {
|
||||||
|
mockSubjectRelatedResourceLinks();
|
||||||
|
when(subject.isAuthenticated()).thenReturn(true);
|
||||||
|
|
||||||
|
when(contextProvider.getVersion()).thenReturn("2.x");
|
||||||
|
|
||||||
|
IndexDto dto = generator.generate();
|
||||||
|
|
||||||
|
assertThat(dto.getLinks().getLinkBy("me")).isPresent();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotAppendMeIfUserIsAuthenticatedButAnonymous() {
|
||||||
|
mockResourceLinks();
|
||||||
|
when(subject.getPrincipal()).thenReturn(USER_ANONYMOUS);
|
||||||
|
when(subject.isAuthenticated()).thenReturn(true);
|
||||||
|
|
||||||
|
IndexDto dto = generator.generate();
|
||||||
|
|
||||||
|
assertThat(dto.getLinks().getLinkBy("me")).isNotPresent();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldAppendMeIfUserIsAnonymousAndAnonymousModeIsFullEnabled() {
|
||||||
|
mockSubjectRelatedResourceLinks();
|
||||||
|
when(subject.getPrincipal()).thenReturn(USER_ANONYMOUS);
|
||||||
|
when(subject.isAuthenticated()).thenReturn(true);
|
||||||
|
when(configuration.getAnonymousMode()).thenReturn(AnonymousMode.FULL);
|
||||||
|
|
||||||
|
IndexDto dto = generator.generate();
|
||||||
|
|
||||||
|
assertThat(dto.getLinks().getLinkBy("me")).isPresent();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotAppendMeIfUserIsAnonymousAndAnonymousModeIsProtocolOnly() {
|
||||||
|
mockResourceLinks();
|
||||||
|
when(subject.getPrincipal()).thenReturn(USER_ANONYMOUS);
|
||||||
|
when(subject.isAuthenticated()).thenReturn(true);
|
||||||
|
when(configuration.getAnonymousMode()).thenReturn(AnonymousMode.PROTOCOL_ONLY);
|
||||||
|
|
||||||
|
IndexDto dto = generator.generate();
|
||||||
|
|
||||||
|
assertThat(dto.getLinks().getLinkBy("me")).isNotPresent();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void mockResourceLinks() {
|
||||||
|
when(resourceLinks.index()).thenReturn(new ResourceLinks.IndexLinks(scmPathInfo));
|
||||||
|
when(resourceLinks.uiPluginCollection()).thenReturn(new ResourceLinks.UIPluginCollectionLinks(scmPathInfo));
|
||||||
|
when(resourceLinks.authentication()).thenReturn(new ResourceLinks.AuthenticationLinks(scmPathInfo));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void mockSubjectRelatedResourceLinks() {
|
||||||
|
mockResourceLinks();
|
||||||
|
when(resourceLinks.repositoryCollection()).thenReturn(new ResourceLinks.RepositoryCollectionLinks(scmPathInfo));
|
||||||
|
when(resourceLinks.repositoryVerbs()).thenReturn(new ResourceLinks.RepositoryVerbLinks(scmPathInfo));
|
||||||
|
when(resourceLinks.repositoryTypeCollection()).thenReturn(new ResourceLinks.RepositoryTypeCollectionLinks(scmPathInfo));
|
||||||
|
when(resourceLinks.repositoryRoleCollection()).thenReturn(new ResourceLinks.RepositoryRoleCollectionLinks(scmPathInfo));
|
||||||
|
when(resourceLinks.namespaceStrategies()).thenReturn(new ResourceLinks.NamespaceStrategiesLinks(scmPathInfo));
|
||||||
|
when(resourceLinks.me()).thenReturn(new ResourceLinks.MeLinks(scmPathInfo, new ResourceLinks.UserLinks(scmPathInfo)));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -198,6 +198,19 @@ class MeDtoFactoryTest {
|
|||||||
assertThat(dto.getLinks().getLinkBy("password")).isNotPresent();
|
assertThat(dto.getLinks().getLinkBy("password")).isNotPresent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldAppendOnlySelfLinkIfAnonymousUser() {
|
||||||
|
User user = SCMContext.ANONYMOUS;
|
||||||
|
prepareSubject(user);
|
||||||
|
|
||||||
|
MeDto dto = meDtoFactory.create();
|
||||||
|
assertThat(dto.getLinks().getLinkBy("self")).isPresent();
|
||||||
|
assertThat(dto.getLinks().getLinkBy("password")).isNotPresent();
|
||||||
|
assertThat(dto.getLinks().getLinkBy("delete")).isNotPresent();
|
||||||
|
assertThat(dto.getLinks().getLinkBy("update")).isNotPresent();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldAppendLinks() {
|
void shouldAppendLinks() {
|
||||||
prepareSubject(UserTestData.createTrillian());
|
prepareSubject(UserTestData.createTrillian());
|
||||||
@@ -213,6 +226,4 @@ class MeDtoFactoryTest {
|
|||||||
MeDto dto = meDtoFactory.create();
|
MeDto dto = meDtoFactory.create();
|
||||||
assertThat(dto.getLinks().getLinkBy("profile").get().getHref()).isEqualTo("http://hitchhiker.com/users/trillian");
|
assertThat(dto.getLinks().getLinkBy("profile").get().getHref()).isEqualTo("http://hitchhiker.com/users/trillian");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user