Authenticate /p/ HTTP and SSH access by password
Use HTTP digest authentication to verify user access to any of the /p/ URLs which do not permit anonymous requests. The SSH daemon now also honors the user's password. Change-Id: I6f8775077b3ee8fcb66a2d07c225f668afa0d530 Signed-off-by: Shawn O. Pearce <sop@google.com>
This commit is contained in:
@@ -0,0 +1,335 @@
|
||||
// Copyright (C) 2010 The Android Open Source Project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.google.gerrit.httpd;
|
||||
|
||||
import static java.util.concurrent.TimeUnit.HOURS;
|
||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
|
||||
|
||||
import com.google.gerrit.server.account.AccountCache;
|
||||
import com.google.gerrit.server.account.AccountState;
|
||||
import com.google.gerrit.server.config.CanonicalWebUrl;
|
||||
import com.google.gerrit.server.config.Nullable;
|
||||
import com.google.gwtjsonrpc.server.SignedToken;
|
||||
import com.google.gwtjsonrpc.server.XsrfException;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Provider;
|
||||
import com.google.inject.Singleton;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.servlet.Filter;
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.FilterConfig;
|
||||
import javax.servlet.ServletContext;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.ServletResponse;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpServletResponseWrapper;
|
||||
|
||||
/**
|
||||
* Authenticates the current user by HTTP digest authentication.
|
||||
* <p>
|
||||
* The current HTTP request is authenticated by looking up the username from the
|
||||
* Authorization header and checking the digest response against the stored
|
||||
* password. This filter is intended only to protect the {@link ProjectServlet}
|
||||
* and its handled URLs, which provide remote repository access over HTTP.
|
||||
*
|
||||
* @see <a href="http://www.ietf.org/rfc/rfc2617.txt">RFC 2617</a>
|
||||
*/
|
||||
@Singleton
|
||||
class ProjectDigestFilter implements Filter {
|
||||
public static final String REALM_NAME = "Gerrit Code Review";
|
||||
private static final String AUTHORIZATION = "Authorization";
|
||||
|
||||
private final Provider<String> urlProvider;
|
||||
private final Provider<WebSession> session;
|
||||
private final AccountCache accountCache;
|
||||
private final SignedToken tokens;
|
||||
private ServletContext context;
|
||||
|
||||
@Inject
|
||||
ProjectDigestFilter(@CanonicalWebUrl @Nullable Provider<String> urlProvider,
|
||||
Provider<WebSession> session, AccountCache accountCache)
|
||||
throws XsrfException {
|
||||
this.urlProvider = urlProvider;
|
||||
this.session = session;
|
||||
this.accountCache = accountCache;
|
||||
this.tokens = new SignedToken((int) SECONDS.convert(1, HOURS));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(FilterConfig config) {
|
||||
context = config.getServletContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response,
|
||||
FilterChain chain) throws IOException, ServletException {
|
||||
HttpServletRequest req = (HttpServletRequest) request;
|
||||
Response rsp = new Response((HttpServletResponse) response);
|
||||
|
||||
if (verify(req, rsp)) {
|
||||
chain.doFilter(req, rsp);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean verify(HttpServletRequest req, Response rsp)
|
||||
throws IOException {
|
||||
final String hdr = req.getHeader(AUTHORIZATION);
|
||||
if (hdr == null) {
|
||||
// Allow an anonymous connection through, or it might be using a
|
||||
// session cookie instead of digest authentication.
|
||||
//
|
||||
return true;
|
||||
}
|
||||
|
||||
final Map<String, String> p = parseAuthorization(hdr);
|
||||
final String username = p.get("username");
|
||||
final String realm = p.get("realm");
|
||||
final String nonce = p.get("nonce");
|
||||
final String uri = p.get("uri");
|
||||
final String response = p.get("response");
|
||||
final String qop = p.get("qop");
|
||||
final String nc = p.get("nc");
|
||||
final String cnonce = p.get("cnonce");
|
||||
final String method = req.getMethod();
|
||||
|
||||
if (username == null //
|
||||
|| realm == null //
|
||||
|| nonce == null //
|
||||
|| uri == null //
|
||||
|| response == null //
|
||||
|| !REALM_NAME.equals(realm)) {
|
||||
context.log("Invalid header: " + AUTHORIZATION + ": " + hdr);
|
||||
rsp.sendError(SC_FORBIDDEN);
|
||||
return false;
|
||||
}
|
||||
|
||||
final AccountState who = accountCache.getByUsername(username);
|
||||
if (who == null) {
|
||||
rsp.sendError(SC_UNAUTHORIZED);
|
||||
return false;
|
||||
}
|
||||
|
||||
final String passwd = who.getPassword(username);
|
||||
if (passwd == null) {
|
||||
rsp.sendError(SC_UNAUTHORIZED);
|
||||
return false;
|
||||
}
|
||||
|
||||
final String A1 = username + ":" + realm + ":" + passwd;
|
||||
final String A2 = method + ":" + uri;
|
||||
|
||||
final String expect;
|
||||
if ("auth".equals(qop)) {
|
||||
expect = KD(H(A1), //
|
||||
nonce + ":" + nc + ":" + cnonce + ":" + qop + ":" + H(A2));
|
||||
} else {
|
||||
expect = KD(H(A1), nonce + ":" + H(A2));
|
||||
}
|
||||
|
||||
if (expect.equals(response)) {
|
||||
try {
|
||||
if (tokens.checkToken(nonce, "") != null) {
|
||||
session.get().setUserAccountId(who.getAccount().getId());
|
||||
return true;
|
||||
|
||||
} else {
|
||||
rsp.stale = true;
|
||||
rsp.sendError(SC_UNAUTHORIZED);
|
||||
return false;
|
||||
}
|
||||
} catch (XsrfException e) {
|
||||
context.log("Error validating nonce for digest authentication", e);
|
||||
rsp.sendError(SC_INTERNAL_SERVER_ERROR);
|
||||
return false;
|
||||
}
|
||||
|
||||
} else {
|
||||
rsp.sendError(SC_UNAUTHORIZED);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static String H(String data) {
|
||||
try {
|
||||
MessageDigest md = newMD5();
|
||||
md.update(data.getBytes("UTF-8"));
|
||||
return LHEX(md.digest());
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new RuntimeException("UTF-8 encoding not available", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String KD(String secret, String data) {
|
||||
try {
|
||||
MessageDigest md = newMD5();
|
||||
md.update(secret.getBytes("UTF-8"));
|
||||
md.update((byte) ':');
|
||||
md.update(data.getBytes("UTF-8"));
|
||||
return LHEX(md.digest());
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new RuntimeException("UTF-8 encoding not available", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static MessageDigest newMD5() {
|
||||
try {
|
||||
return MessageDigest.getInstance("MD5");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException("No MD5 available", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static final char[] LHEX =
|
||||
{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', //
|
||||
'a', 'b', 'c', 'd', 'e', 'f'};
|
||||
|
||||
private static String LHEX(byte[] bin) {
|
||||
StringBuilder r = new StringBuilder(bin.length * 2);
|
||||
for (int i = 0; i < bin.length; i++) {
|
||||
byte b = bin[i];
|
||||
r.append(LHEX[(b >>> 4) & 0x0f]);
|
||||
r.append(LHEX[b & 0x0f]);
|
||||
}
|
||||
return r.toString();
|
||||
}
|
||||
|
||||
private Map<String, String> parseAuthorization(String auth) {
|
||||
if (!auth.startsWith("Digest ")) {
|
||||
// We only support Digest authentication scheme, deny the rest.
|
||||
//
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
Map<String, String> p = new HashMap<String, String>();
|
||||
int next = "Digest ".length();
|
||||
while (next < auth.length()) {
|
||||
if (next < auth.length() && auth.charAt(next) == ',') {
|
||||
next++;
|
||||
}
|
||||
while (next < auth.length() && Character.isWhitespace(auth.charAt(next))) {
|
||||
next++;
|
||||
}
|
||||
|
||||
int eq = auth.indexOf('=', next);
|
||||
if (eq < 0 || eq + 1 == auth.length()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
final String name = auth.substring(next, eq);
|
||||
final String value;
|
||||
if (auth.charAt(eq + 1) == '"') {
|
||||
int dq = auth.indexOf('"', eq + 2);
|
||||
if (dq < 0) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
value = auth.substring(eq + 2, dq);
|
||||
next = dq + 1;
|
||||
|
||||
} else {
|
||||
int space = auth.indexOf(' ', eq + 1);
|
||||
int comma = auth.indexOf(',', eq + 1);
|
||||
if (space < 0) space = auth.length();
|
||||
if (comma < 0) comma = auth.length();
|
||||
|
||||
final int e = Math.min(space, comma);
|
||||
value = auth.substring(eq + 1, e);
|
||||
next = e + 1;
|
||||
}
|
||||
p.put(name, value);
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
private String getDomain() {
|
||||
return urlProvider.get() + "p/";
|
||||
}
|
||||
|
||||
private String newNonce() {
|
||||
try {
|
||||
return tokens.newToken("");
|
||||
} catch (XsrfException e) {
|
||||
throw new RuntimeException("Cannot generate new nonce", e);
|
||||
}
|
||||
}
|
||||
|
||||
class Response extends HttpServletResponseWrapper {
|
||||
private static final String WWW_AUTHENTICATE = "WWW-Authenticate";
|
||||
|
||||
Boolean stale;
|
||||
|
||||
Response(HttpServletResponse rsp) {
|
||||
super(rsp);
|
||||
}
|
||||
|
||||
private void status(int sc) {
|
||||
if (sc == SC_UNAUTHORIZED) {
|
||||
StringBuilder v = new StringBuilder();
|
||||
v.append("Digest");
|
||||
v.append(" realm=\"" + REALM_NAME + "\"");
|
||||
v.append(", domain=\"" + getDomain() + "\"");
|
||||
v.append(", qop=\"auth\"");
|
||||
if (stale != null) {
|
||||
v.append(", stale=" + stale);
|
||||
}
|
||||
v.append(", nonce=\"" + newNonce() + "\"");
|
||||
setHeader(WWW_AUTHENTICATE, v.toString());
|
||||
|
||||
} else if (containsHeader(WWW_AUTHENTICATE)) {
|
||||
setHeader(WWW_AUTHENTICATE, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendError(int sc, String msg) throws IOException {
|
||||
status(sc);
|
||||
super.sendError(sc, msg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendError(int sc) throws IOException {
|
||||
status(sc);
|
||||
super.sendError(sc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setStatus(int sc, String sm) {
|
||||
status(sc);
|
||||
super.setStatus(sc, sm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setStatus(int sc) {
|
||||
status(sc);
|
||||
super.setStatus(sc);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ package com.google.gerrit.httpd;
|
||||
import com.google.gerrit.common.PageLinks;
|
||||
import com.google.gerrit.reviewdb.Change;
|
||||
import com.google.gerrit.reviewdb.Project;
|
||||
import com.google.gerrit.server.AnonymousUser;
|
||||
import com.google.gerrit.server.IdentifiedUser;
|
||||
import com.google.gerrit.server.config.CanonicalWebUrl;
|
||||
import com.google.gerrit.server.config.Nullable;
|
||||
@@ -131,7 +132,8 @@ public class ProjectServlet extends GitServlet {
|
||||
|
||||
@Override
|
||||
public Repository open(HttpServletRequest req, String projectName)
|
||||
throws RepositoryNotFoundException {
|
||||
throws RepositoryNotFoundException, ServiceNotAuthorizedException,
|
||||
ServiceNotEnabledException {
|
||||
if (projectName.endsWith(".git")) {
|
||||
// Be nice and drop the trailing ".git" suffix, which we never keep
|
||||
// in our database, but clients might mistakenly provide anyway.
|
||||
@@ -151,10 +153,17 @@ public class ProjectServlet extends GitServlet {
|
||||
final ProjectControl pc;
|
||||
try {
|
||||
final Project.NameKey nameKey = new Project.NameKey(projectName);
|
||||
pc = projectControlFactory.validateFor(nameKey);
|
||||
pc = projectControlFactory.controlFor(nameKey);
|
||||
} catch (NoSuchProjectException err) {
|
||||
throw new RepositoryNotFoundException(projectName);
|
||||
}
|
||||
if (!pc.isVisible()) {
|
||||
if (pc.getCurrentUser() instanceof AnonymousUser) {
|
||||
throw new ServiceNotAuthorizedException();
|
||||
} else {
|
||||
throw new ServiceNotEnabledException();
|
||||
}
|
||||
}
|
||||
req.setAttribute(ATT_CONTROL, pc);
|
||||
|
||||
return manager.openRepository(pc.getProject().getName());
|
||||
|
||||
@@ -49,6 +49,8 @@ class UrlModule extends ServletModule {
|
||||
serve("/signout").with(HttpLogoutServlet.class);
|
||||
serve("/ssh_info").with(SshInfoServlet.class);
|
||||
serve("/static/*").with(StaticServlet.class);
|
||||
|
||||
filter("/p/*").through(ProjectDigestFilter.class);
|
||||
serve("/p/*").with(ProjectServlet.class);
|
||||
|
||||
serve("/Main.class").with(notFound());
|
||||
|
||||
@@ -148,6 +148,12 @@ public final class WebSession {
|
||||
saveCookie();
|
||||
}
|
||||
|
||||
/** Set the user account for this current request only. */
|
||||
void setUserAccountId(Account.Id id) {
|
||||
key = new Key("id:" + id);
|
||||
val = new Val(id, 0, false, null);
|
||||
}
|
||||
|
||||
public void logout() {
|
||||
if (val != null) {
|
||||
manager.destroy(key);
|
||||
|
||||
@@ -20,5 +20,9 @@ import com.google.gerrit.reviewdb.Account;
|
||||
public interface AccountCache {
|
||||
public AccountState get(Account.Id accountId);
|
||||
|
||||
public AccountState getByUsername(String username);
|
||||
|
||||
public void evict(Account.Id accountId);
|
||||
|
||||
public void evictByUsername(String username);
|
||||
}
|
||||
|
||||
@@ -45,8 +45,8 @@ public class AccountCacheImpl implements AccountCache {
|
||||
return new CacheModule() {
|
||||
@Override
|
||||
protected void configure() {
|
||||
final TypeLiteral<Cache<Account.Id, AccountState>> type =
|
||||
new TypeLiteral<Cache<Account.Id, AccountState>>() {};
|
||||
final TypeLiteral<Cache<Object, AccountState>> type =
|
||||
new TypeLiteral<Cache<Object, AccountState>>() {};
|
||||
core(type, CACHE_NAME);
|
||||
bind(AccountCacheImpl.class);
|
||||
bind(AccountCache.class).to(AccountCacheImpl.class);
|
||||
@@ -56,7 +56,8 @@ public class AccountCacheImpl implements AccountCache {
|
||||
|
||||
private final SchemaFactory<ReviewDb> schema;
|
||||
private final GroupCache groupCache;
|
||||
private final SelfPopulatingCache<Account.Id, AccountState> self;
|
||||
private final SelfPopulatingCache<Account.Id, AccountState> byId;
|
||||
private final SelfPopulatingCache<String, Account.Id> byName;
|
||||
|
||||
private final Set<AccountGroup.Id> registered;
|
||||
private final Set<AccountGroup.Id> anonymous;
|
||||
@@ -64,13 +65,13 @@ public class AccountCacheImpl implements AccountCache {
|
||||
@Inject
|
||||
AccountCacheImpl(final SchemaFactory<ReviewDb> sf, final AuthConfig auth,
|
||||
final GroupCache groupCache,
|
||||
@Named(CACHE_NAME) final Cache<Account.Id, AccountState> rawCache) {
|
||||
@Named(CACHE_NAME) final Cache<Object, AccountState> rawCache) {
|
||||
schema = sf;
|
||||
registered = auth.getRegisteredGroups();
|
||||
anonymous = auth.getAnonymousGroups();
|
||||
this.groupCache = groupCache;
|
||||
|
||||
self = new SelfPopulatingCache<Account.Id, AccountState>(rawCache) {
|
||||
byId = new SelfPopulatingCache<Account.Id, AccountState>((Cache) rawCache) {
|
||||
@Override
|
||||
protected AccountState createEntry(Account.Id key) throws Exception {
|
||||
return lookup(key);
|
||||
@@ -81,11 +82,42 @@ public class AccountCacheImpl implements AccountCache {
|
||||
return missingAccount(key);
|
||||
}
|
||||
};
|
||||
|
||||
byName = new SelfPopulatingCache<String, Account.Id>((Cache) rawCache) {
|
||||
@Override
|
||||
protected Account.Id createEntry(String username) throws Exception {
|
||||
return lookup(username);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private AccountState lookup(final Account.Id who) throws OrmException {
|
||||
final ReviewDb db = schema.open();
|
||||
try {
|
||||
final AccountState state = load(db, who);
|
||||
if (state.getUserName() != null) {
|
||||
byName.put(state.getUserName(), state.getAccount().getId());
|
||||
}
|
||||
return state;
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
|
||||
private Account.Id lookup(final String username) throws OrmException {
|
||||
final ReviewDb db = schema.open();
|
||||
try {
|
||||
final AccountExternalId.Key key =
|
||||
new AccountExternalId.Key(AccountExternalId.SCHEME_USERNAME, username);
|
||||
final AccountExternalId id = db.accountExternalIds().get(key);
|
||||
return id != null ? id.getAccountId() : null;
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
|
||||
private AccountState load(final ReviewDb db, final Account.Id who)
|
||||
throws OrmException {
|
||||
final Account account = db.accounts().get(who);
|
||||
if (account == null) {
|
||||
// Account no longer exists? They are anonymous.
|
||||
@@ -114,9 +146,6 @@ public class AccountCacheImpl implements AccountCache {
|
||||
}
|
||||
|
||||
return new AccountState(account, internalGroups, externalIds);
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
|
||||
private AccountState missingAccount(final Account.Id accountId) {
|
||||
@@ -126,10 +155,20 @@ public class AccountCacheImpl implements AccountCache {
|
||||
}
|
||||
|
||||
public AccountState get(final Account.Id accountId) {
|
||||
return self.get(accountId);
|
||||
return byId.get(accountId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccountState getByUsername(String username) {
|
||||
Account.Id id = byName.get(username);
|
||||
return id != null ? get(id) : null;
|
||||
}
|
||||
|
||||
public void evict(final Account.Id accountId) {
|
||||
self.remove(accountId);
|
||||
byId.remove(accountId);
|
||||
}
|
||||
|
||||
public void evictByUsername(String username) {
|
||||
byName.remove(username);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,17 @@ public class AccountState {
|
||||
return account.getUserName();
|
||||
}
|
||||
|
||||
/** @return the password matching the requested username; or null. */
|
||||
public String getPassword(String username) {
|
||||
for (AccountExternalId id : getExternalIds()) {
|
||||
if (id.isScheme(AccountExternalId.SCHEME_USERNAME)
|
||||
&& username.equals(id.getSchemeRest())) {
|
||||
return id.getPassword();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* All email addresses registered to this account.
|
||||
* <p>
|
||||
|
||||
@@ -127,9 +127,11 @@ public class ChangeUserName implements Callable<VoidResult> {
|
||||
db.accountExternalIds().delete(old);
|
||||
for (AccountExternalId i : old) {
|
||||
sshKeyCache.evict(i.getSchemeRest());
|
||||
accountCache.evictByUsername(i.getSchemeRest());
|
||||
}
|
||||
|
||||
accountCache.evict(user.getAccountId());
|
||||
accountCache.evictByUsername(newUsername);
|
||||
sshKeyCache.evict(newUsername);
|
||||
return VoidResult.INSTANCE;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
// Copyright (C) 2010 The Android Open Source Project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.google.gerrit.sshd;
|
||||
|
||||
import static com.google.gerrit.sshd.SshUtil.AUTH_ATTEMPTED_AS;
|
||||
import static com.google.gerrit.sshd.SshUtil.AUTH_ERROR;
|
||||
import static com.google.gerrit.sshd.SshUtil.CURRENT_ACCOUNT;
|
||||
|
||||
import com.google.gerrit.reviewdb.AccountExternalId;
|
||||
import com.google.gerrit.server.account.AccountCache;
|
||||
import com.google.gerrit.server.account.AccountState;
|
||||
import com.google.gerrit.sshd.SshScopes.Context;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
|
||||
import org.apache.mina.core.future.IoFuture;
|
||||
import org.apache.mina.core.future.IoFutureListener;
|
||||
import org.apache.sshd.server.PasswordAuthenticator;
|
||||
import org.apache.sshd.server.session.ServerSession;
|
||||
|
||||
/**
|
||||
* Authenticates by password through {@link AccountExternalId} entities.
|
||||
*/
|
||||
@Singleton
|
||||
class DatabasePasswordAuth implements PasswordAuthenticator {
|
||||
private final AccountCache accountCache;
|
||||
private final SshLog log;
|
||||
|
||||
@Inject
|
||||
DatabasePasswordAuth(final AccountCache ac, final SshLog l) {
|
||||
accountCache = ac;
|
||||
log = l;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean authenticate(final String username, final String password,
|
||||
final ServerSession session) {
|
||||
AccountState state = accountCache.getByUsername(username);
|
||||
if (state == null) {
|
||||
return fail(username, session, "user-not-found");
|
||||
}
|
||||
|
||||
final String p = state.getPassword(username);
|
||||
if (p == null) {
|
||||
return fail(username, session, "no-password");
|
||||
}
|
||||
|
||||
if (!p.equals(password)) {
|
||||
return fail(username, session, "incorrect-password");
|
||||
}
|
||||
|
||||
if (session.setAttribute(CURRENT_ACCOUNT, state.getAccount().getId()) == null) {
|
||||
// If this is the first time we've authenticated this
|
||||
// session, record a login event in the log and add
|
||||
// a close listener to record a logout event.
|
||||
//
|
||||
final Context ctx = new Context(session);
|
||||
final Context old = SshScopes.current.get();
|
||||
try {
|
||||
SshScopes.current.set(ctx);
|
||||
log.onLogin();
|
||||
} finally {
|
||||
SshScopes.current.set(old);
|
||||
}
|
||||
|
||||
session.getIoSession().getCloseFuture().addListener(
|
||||
new IoFutureListener<IoFuture>() {
|
||||
@Override
|
||||
public void operationComplete(IoFuture future) {
|
||||
final Context old = SshScopes.current.get();
|
||||
try {
|
||||
SshScopes.current.set(ctx);
|
||||
log.onLogout();
|
||||
} finally {
|
||||
SshScopes.current.set(old);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean fail(final String username,
|
||||
final ServerSession session, final String err) {
|
||||
session.setAttribute(AUTH_ATTEMPTED_AS, username);
|
||||
session.setAttribute(AUTH_ERROR, err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -32,10 +32,6 @@ import java.security.PublicKey;
|
||||
|
||||
/**
|
||||
* Authenticates by public key through {@link AccountSshKey} entities.
|
||||
* <p>
|
||||
* The username supplied by the client must be the user's preferred email
|
||||
* address, as listed in their Account entity. Only keys listed under that
|
||||
* account as authorized keys are permitted to access the account.
|
||||
*/
|
||||
@Singleton
|
||||
class DatabasePubKeyAuth implements PublickeyAuthenticator {
|
||||
|
||||
@@ -60,8 +60,10 @@ import org.apache.sshd.common.util.SecurityUtils;
|
||||
import org.apache.sshd.server.Command;
|
||||
import org.apache.sshd.server.CommandFactory;
|
||||
import org.apache.sshd.server.ForwardingFilter;
|
||||
import org.apache.sshd.server.PasswordAuthenticator;
|
||||
import org.apache.sshd.server.PublickeyAuthenticator;
|
||||
import org.apache.sshd.server.UserAuth;
|
||||
import org.apache.sshd.server.auth.UserAuthPassword;
|
||||
import org.apache.sshd.server.auth.UserAuthPublicKey;
|
||||
import org.apache.sshd.server.channel.ChannelDirectTcpip;
|
||||
import org.apache.sshd.server.channel.ChannelSession;
|
||||
@@ -119,6 +121,7 @@ public class SshDaemon extends SshServer implements SshInfo, LifecycleListener {
|
||||
|
||||
@Inject
|
||||
SshDaemon(final CommandFactory commandFactory,
|
||||
final PasswordAuthenticator passAuth,
|
||||
final PublickeyAuthenticator userAuth,
|
||||
final KeyPairProvider hostKeyProvider, final IdGenerator idGenerator,
|
||||
@GerritServerConfig final Config cfg, final SshLog sshLog) {
|
||||
@@ -140,7 +143,7 @@ public class SshDaemon extends SshServer implements SshInfo, LifecycleListener {
|
||||
initForwardingFilter();
|
||||
initSubsystems();
|
||||
initCompression();
|
||||
initUserAuth(userAuth);
|
||||
initUserAuth(passAuth, userAuth);
|
||||
setKeyPairProvider(hostKeyProvider);
|
||||
setCommandFactory(commandFactory);
|
||||
setShellFactory(new NoShell());
|
||||
@@ -452,9 +455,11 @@ public class SshDaemon extends SshServer implements SshInfo, LifecycleListener {
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void initUserAuth(final PublickeyAuthenticator pubkey) {
|
||||
setUserAuthFactories(Arrays
|
||||
.<NamedFactory<UserAuth>> asList(new UserAuthPublicKey.Factory()));
|
||||
private void initUserAuth(final PasswordAuthenticator pass,
|
||||
final PublickeyAuthenticator pubkey) {
|
||||
setUserAuthFactories(Arrays.<NamedFactory<UserAuth>> asList(
|
||||
new UserAuthPublicKey.Factory(), new UserAuthPassword.Factory()));
|
||||
setPasswordAuthenticator(pass);
|
||||
setPublickeyAuthenticator(pubkey);
|
||||
}
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ import com.google.inject.servlet.SessionScoped;
|
||||
import org.apache.sshd.common.KeyPairProvider;
|
||||
import org.apache.sshd.common.session.AbstractSession;
|
||||
import org.apache.sshd.server.CommandFactory;
|
||||
import org.apache.sshd.server.PasswordAuthenticator;
|
||||
import org.apache.sshd.server.PublickeyAuthenticator;
|
||||
import org.apache.sshd.server.session.ServerSession;
|
||||
import org.kohsuke.args4j.spi.OptionHandler;
|
||||
@@ -81,6 +82,7 @@ public class SshModule extends FactoryModule {
|
||||
.toProvider(CommandExecutorProvider.class).in(SINGLETON);
|
||||
|
||||
bind(PublickeyAuthenticator.class).to(DatabasePubKeyAuth.class);
|
||||
bind(PasswordAuthenticator.class).to(DatabasePasswordAuth.class);
|
||||
bind(KeyPairProvider.class).toProvider(HostKeyProvider.class).in(SINGLETON);
|
||||
|
||||
install(new DefaultCommandModule());
|
||||
|
||||
Reference in New Issue
Block a user