Merge changes I4fad66f5,I6ddb8258

* changes:
  Settings screen for OAuth tokens
  REST API for retrieving OAuth access tokens
This commit is contained in:
Saša Živkov 2016-06-14 14:33:51 +00:00 committed by Gerrit Code Review
commit 4693626108
14 changed files with 487 additions and 3 deletions

View File

@ -420,6 +420,43 @@ Deletes the HTTP password of an account.
HTTP/1.1 204 No Content
----
[[get-oauth-token]]
=== Get OAuth Access Token
--
'GET /accounts/link:#account-id[\{account-id\}]/oauthtoken'
--
Returns a previously obtained OAuth access token.
.Request
----
GET /accounts/self/oauthtoken HTTP/1.1
----
As a response, an link:#oauth-token-info[OAuthTokenInfo] entity is returned
that describes the OAuth access token.
.Response
----
HTTP/1.1 200 OK
Content-Disposition: attachment
Content-Type: application/json; charset=UTF-8
)]}'
{
"username": "johndow",
"resource_host": "gerrit.example.org",
"access_token": "eyJhbGciOiJSUzI1NiJ9.eyJqdGkiOi",
"provider_id": "oauth-plugin:oauth-provider",
"expires_at": "922337203775807",
"type": "bearer"
}
----
If there is no token available, or the token has already expired,
"`404 Not Found`" is returned as response. Requests to obtain an access
token of another user are rejected with "`403 Forbidden`".
[[list-account-emails]]
=== List Account Emails
--
@ -2214,6 +2251,22 @@ If empty or not set and `generate` is false or not set, the HTTP
password is deleted.
|============================
[[oauth-token-info]]
=== OAuthTokenInfo
The `OAuthTokenInfo` entity contains information about an OAuth access token.
[options="header",cols="1,^1,5"]
|========================
|Field Name ||Description
|`username` ||The owner of the OAuth access token.
|`resource_host` ||The host of the Gerrit instance.
|`access_token` ||The actual token value.
|`provider_id` |optional|
The identifier of the OAuth provider in the form `plugin-name:provider-name`.
|`expires_at` |optional|Time of expiration of this token in milliseconds.
|`type` ||The type of the OAuth access token, always `bearer`.
|========================
[[preferences-info]]
=== PreferencesInfo
The `PreferencesInfo` entity contains information about a user's preferences.

View File

@ -29,6 +29,7 @@ public class PageLinks {
public static final String SETTINGS_SSHKEYS = "/settings/ssh-keys";
public static final String SETTINGS_GPGKEYS = "/settings/gpg-keys";
public static final String SETTINGS_HTTP_PASSWORD = "/settings/http-password";
public static final String SETTINGS_OAUTH_TOKEN = "/settings/oauth-token";
public static final String SETTINGS_WEBIDENT = "/settings/web-identities";
public static final String SETTINGS_MYGROUPS = "/settings/group-memberships";
public static final String SETTINGS_AGREEMENTS = "/settings/agreements";

View File

@ -31,16 +31,23 @@ public class OAuthToken implements Serializable {
*/
private final long expiresAt;
/**
* The identifier of the OAuth provider that issued this token
* in the form <tt>"plugin-name:provider-name"</tt>, or {@code null}.
*/
private final String providerId;
public OAuthToken(String token, String secret, String raw) {
this(token, secret, raw, Long.MAX_VALUE);
this(token, secret, raw, Long.MAX_VALUE, null);
}
public OAuthToken(String token, String secret, String raw,
long expiresAt) {
long expiresAt, String providerId) {
this.token = token;
this.secret = secret;
this.raw = raw;
this.expiresAt = expiresAt;
this.providerId = providerId;
}
public String getToken() {
@ -62,4 +69,8 @@ public class OAuthToken implements Serializable {
public boolean isExpired() {
return System.currentTimeMillis() > expiresAt;
}
public String getProviderId() {
return providerId;
}
}

View File

@ -0,0 +1,31 @@
// Copyright (C) 2016 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.client.info;
import com.google.gwt.core.client.JavaScriptObject;
public class OAuthTokenInfo extends JavaScriptObject {
protected OAuthTokenInfo() {
}
public final native String username() /*-{ return this.username; }-*/;
public final native String resourceHost() /*-{ return this.resource_host; }-*/;
public final native String accessToken() /*-{ return this.access_token; }-*/;
public final native String providerId() /*-{ return this.provider_id; }-*/;
public final native String expiresAt() /*-{ return this.expires_at; }-*/;
public final native String type() /*-{ return this.type; }-*/;
}

View File

@ -33,6 +33,7 @@ import static com.google.gerrit.common.PageLinks.SETTINGS_EDIT_PREFERENCES;
import static com.google.gerrit.common.PageLinks.SETTINGS_EXTENSION;
import static com.google.gerrit.common.PageLinks.SETTINGS_GPGKEYS;
import static com.google.gerrit.common.PageLinks.SETTINGS_HTTP_PASSWORD;
import static com.google.gerrit.common.PageLinks.SETTINGS_OAUTH_TOKEN;
import static com.google.gerrit.common.PageLinks.SETTINGS_MYGROUPS;
import static com.google.gerrit.common.PageLinks.SETTINGS_NEW_AGREEMENT;
import static com.google.gerrit.common.PageLinks.SETTINGS_PREFERENCES;
@ -48,6 +49,7 @@ import com.google.gerrit.client.account.MyEditPreferencesScreen;
import com.google.gerrit.client.account.MyGpgKeysScreen;
import com.google.gerrit.client.account.MyGroupsScreen;
import com.google.gerrit.client.account.MyIdentitiesScreen;
import com.google.gerrit.client.account.MyOAuthTokenScreen;
import com.google.gerrit.client.account.MyPasswordScreen;
import com.google.gerrit.client.account.MyPreferencesScreen;
import com.google.gerrit.client.account.MyProfileScreen;
@ -568,6 +570,12 @@ public class Dispatcher {
return new MyPasswordScreen();
}
if (matchExact(SETTINGS_OAUTH_TOKEN, token)
&& Gerrit.info().auth().isOAuth()
&& Gerrit.info().auth().isGitBasicAuth()) {
return new MyOAuthTokenScreen();
}
if (matchExact(MY_GROUPS, token)
|| matchExact(SETTINGS_MYGROUPS, token)) {
return new MyGroupsScreen();

View File

@ -105,6 +105,14 @@ public interface GerritCss extends CssResource {
String menuScreenMenuBar();
String needsReview();
String negscore();
String oauthExpires();
String oauthInfoBlock();
String oauthPanel();
String oauthPanelCookieEntry();
String oauthPanelCookieHeading();
String oauthPanelNetRCEntry();
String oauthPanelNetRCHeading();
String oauthToken();
String pagingLink();
String patchSetActions();
String pluginProjectConfigInheritedValue();

View File

@ -57,6 +57,7 @@ public interface AccountConstants extends Constants {
String tabGpgKeys();
String tabHttpAccess();
String tabMyGroups();
String tabOAuthToken();
String tabPreferences();
String tabSshKeys();
String tabWatchedProjects();
@ -81,6 +82,12 @@ public interface AccountConstants extends Constants {
String invalidUserName();
String invalidUserEmail();
String labelOAuthToken();
String labelOAuthExpires();
String labelOAuthNetRCEntry();
String labelOAuthGitCookie();
String labelOAuthExpired();
String sshKeyInvalid();
String sshKeyAlgorithm();
String sshKeyKey();

View File

@ -44,6 +44,7 @@ tabDiffPreferences = Diff Preferences
tabEditPreferences = Edit Preferences
tabGpgKeys = GPG Public Keys
tabHttpAccess = HTTP Password
tabOAuthToken = OAuth Token
tabMyGroups = Groups
tabPreferences = Preferences
tabSshKeys = SSH Public Keys
@ -68,6 +69,13 @@ linkEditFullName = Edit
linkReloadContact = Reload
invalidUserName = Username must contain only letters, numbers, _, - or .
invalidUserEmail = Email format is wrong.
labelOAuthToken = Access Token
labelOAuthExpires = Expires
labelOAuthNetRCEntry = Entry for ~/.netrc
labelOAuthGitCookie = Entry for ~/.gitcookies
labelOAuthExpired = To obtain an access token please sign out and sign in again.
sshKeyInvalid = Invalid Key
sshKeyAlgorithm = Algorithm
sshKeyKey = Key

View File

@ -0,0 +1,197 @@
// Copyright (C) 2016 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.client.account;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.info.GeneralPreferences;
import com.google.gerrit.client.info.OAuthTokenInfo;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.rpc.ScreenLoadCallback;
import com.google.gwt.i18n.client.DateTimeFormat;
import com.google.gwt.i18n.client.LocaleInfo;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.Grid;
import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwtexpui.clippy.client.CopyableLabel;
import java.util.Date;
public class MyOAuthTokenScreen extends SettingsScreen {
private CopyableLabel tokenLabel;
private Label expiresLabel;
private Label expiredNote;
private CopyableLabel netrcValue;
private CopyableLabel cookieValue;
private FlowPanel flow;
private Grid grid;
@Override
protected void onInitUI() {
super.onInitUI();
tokenLabel = new CopyableLabel("");
tokenLabel.addStyleName(Gerrit.RESOURCES.css().oauthToken());
expiresLabel = new Label("");
expiresLabel.addStyleName(Gerrit.RESOURCES.css().oauthExpires());
grid = new Grid(2, 2);
grid.setStyleName(Gerrit.RESOURCES.css().infoBlock());
grid.addStyleName(Gerrit.RESOURCES.css().oauthInfoBlock());
add(grid);
expiredNote = new Label(Util.C.labelOAuthExpired());
expiredNote.setVisible(false);
add(expiredNote);
row(grid, 0, Util.C.labelOAuthToken(), tokenLabel);
row(grid, 1, Util.C.labelOAuthExpires(), expiresLabel);
CellFormatter fmt = grid.getCellFormatter();
fmt.addStyleName(0, 0, Gerrit.RESOURCES.css().topmost());
fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().topmost());
fmt.addStyleName(1, 0, Gerrit.RESOURCES.css().bottomheader());
flow = new FlowPanel();
flow.setStyleName(Gerrit.RESOURCES.css().oauthPanel());
add(flow);
Label netrcLabel = new Label(Util.C.labelOAuthNetRCEntry());
netrcLabel.setStyleName(Gerrit.RESOURCES.css().oauthPanelNetRCHeading());
flow.add(netrcLabel);
netrcValue= new CopyableLabel("");
netrcValue.setStyleName(Gerrit.RESOURCES.css().oauthPanelNetRCEntry());
flow.add(netrcValue);
Label cookieLabel = new Label(Util.C.labelOAuthGitCookie());
cookieLabel.setStyleName(Gerrit.RESOURCES.css().oauthPanelCookieHeading());
flow.add(cookieLabel);
cookieValue = new CopyableLabel("");
cookieValue.setStyleName(Gerrit.RESOURCES.css().oauthPanelCookieEntry());
flow.add(cookieValue);
}
private void row(Grid grid, int row, String name, Widget field) {
final CellFormatter fmt = grid.getCellFormatter();
if (LocaleInfo.getCurrentLocale().isRTL()) {
grid.setText(row, 1, name);
grid.setWidget(row, 0, field);
fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().header());
} else {
grid.setText(row, 0, name);
grid.setWidget(row, 1, field);
fmt.addStyleName(row, 0, Gerrit.RESOURCES.css().header());
}
}
@Override
protected void onLoad() {
super.onLoad();
AccountApi.self().view("preferences")
.get(new ScreenLoadCallback<GeneralPreferences>(this) {
@Override
protected void preDisplay(GeneralPreferences prefs) {
display(prefs);
}
});
}
private void display(final GeneralPreferences prefs) {
AccountApi.self().view("oauthtoken")
.get(new GerritCallback<OAuthTokenInfo>() {
@Override
public void onSuccess(OAuthTokenInfo tokenInfo) {
tokenLabel.setText(tokenInfo.accessToken());
expiresLabel.setText(getExpiresAt(tokenInfo, prefs));
netrcValue.setText(getNetRC(tokenInfo));
cookieValue.setText(getCookie(tokenInfo));
flow.setVisible(true);
expiredNote.setVisible(false);
}
@Override
public void onFailure(Throwable caught) {
if (isNoSuchEntity(caught) || isSigninFailure(caught)) {
tokenLabel.setText("");
expiresLabel.setText("");
netrcValue.setText("");
cookieValue.setText("");
flow.setVisible(false);
expiredNote.setVisible(true);
} else {
showFailure(caught);
}
}
});
}
private static long getExpiresAt(OAuthTokenInfo tokenInfo) {
if (tokenInfo.expiresAt() == null) {
return Long.MAX_VALUE;
}
long expiresAt;
try {
expiresAt = Long.parseLong(tokenInfo.expiresAt());
} catch (NumberFormatException e) {
return Long.MAX_VALUE;
}
return expiresAt;
}
private static long getExpiresAtSeconds(OAuthTokenInfo tokenInfo) {
return getExpiresAt(tokenInfo) / 1000L;
}
private static String getExpiresAt(OAuthTokenInfo tokenInfo,
GeneralPreferences prefs) {
long expiresAt = getExpiresAt(tokenInfo);
if (expiresAt == Long.MAX_VALUE) {
return "";
}
String dateFormat = prefs.dateFormat().getLongFormat();
String timeFormat = prefs.timeFormat().getFormat();
DateTimeFormat formatter = DateTimeFormat.getFormat(
dateFormat + " " + timeFormat);
return formatter.format(new Date(expiresAt));
}
private static String getNetRC(OAuthTokenInfo accessTokenInfo) {
StringBuilder sb = new StringBuilder();
sb.append("machine ");
sb.append(accessTokenInfo.resourceHost());
sb.append(" login ");
sb.append(accessTokenInfo.username());
sb.append(" password ");
sb.append(accessTokenInfo.accessToken());
return sb.toString();
}
private static String getCookie(OAuthTokenInfo accessTokenInfo) {
StringBuilder sb = new StringBuilder();
sb.append(accessTokenInfo.resourceHost());
sb.append("\tFALSE\t/\tTRUE\t");
sb.append(getExpiresAtSeconds(accessTokenInfo));
sb.append("\tgit-");
sb.append(accessTokenInfo.username());
sb.append('\t');
sb.append(accessTokenInfo.accessToken());
if (accessTokenInfo.providerId() != null) {
sb.append('@').append(accessTokenInfo.providerId());
}
return sb.toString();
}
}

View File

@ -47,6 +47,10 @@ public abstract class SettingsScreen extends MenuScreen {
if (Gerrit.info().auth().isHttpPasswordSettingsEnabled()) {
linkByGerrit(Util.C.tabHttpAccess(), PageLinks.SETTINGS_HTTP_PASSWORD);
}
if (Gerrit.info().auth().isOAuth()
&& Gerrit.info().auth().isGitBasicAuth()) {
linkByGerrit(Util.C.tabOAuthToken(), PageLinks.SETTINGS_OAUTH_TOKEN);
}
if (Gerrit.info().gerrit().editGpgKeys()) {
linkByGerrit(Util.C.tabGpgKeys(), PageLinks.SETTINGS_GPGKEYS);
}

View File

@ -828,6 +828,70 @@ a:hover.downloadLink {
margin-bottom: 10px;
}
.oauthInfoBlock {
margin-bottom: 10px;
}
.oauthToken {
font-family: monospace;
font-size: small;
width: 40em;
}
.oauthToken span {
white-space: nowrap;
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
width: 38em;
}
.oauthExpires {
font-family: monospace;
font-size: small;
width: 40em;
}
.oauthPanel {
margin-top: 10px;
border: 1px solid trimColor;
padding: 5px 5px 5px 5px;
}
.oauthPanelNetRCHeading {
margin-top: 5px;
margin-left: 1em;
white-space: nowrap;
}
.oauthPanelNetRCEntry {
margin-top: 5px;
margin-left: 2em;
font-family: monospace;
font-size: small;
width: 80em;
}
.oauthPanelNetRCEntry span {
white-space: nowrap;
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
width: 78em;
}
.oauthPanelCookieHeading {
margin-top: 15px;
margin-left: 1em;
white-space: nowrap;
}
.oauthPanelCookieEntry {
margin-top: 5px;
margin-left: 2em;
font-family: monospace;
font-size: small;
width: 80em;
}
.oauthPanelCookieEntry span {
white-space: nowrap;
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
width: 78em;
}
/** CommentedActionDialog **/
.commentedActionDialog .gwt-DisclosurePanel .header td {

View File

@ -0,0 +1,90 @@
// Copyright (C) 2016 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.server.account;
import com.google.gerrit.extensions.auth.oauth.OAuthToken;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.auth.oauth.OAuthTokenCache;
import com.google.gerrit.server.config.CanonicalWebUrl;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.net.URI;
import java.net.URISyntaxException;
@Singleton
class GetOAuthToken implements RestReadView<AccountResource>{
private static final String BEARER_TYPE = "bearer";
private final Provider<CurrentUser> self;
private final OAuthTokenCache tokenCache;
private final String hostName;
@Inject
GetOAuthToken(Provider<CurrentUser> self,
OAuthTokenCache tokenCache,
@CanonicalWebUrl Provider<String> urlProvider) {
this.self = self;
this.tokenCache = tokenCache;
this.hostName = getHostName(urlProvider.get());
}
@Override
public OAuthTokenInfo apply(AccountResource rsrc) throws AuthException,
ResourceNotFoundException {
if (self.get() != rsrc.getUser()) {
throw new AuthException("not allowed to get access token");
}
String username = rsrc.getUser().getAccount().getUserName();
if (username == null) {
throw new ResourceNotFoundException();
}
OAuthToken accessToken = tokenCache.get(username);
if (accessToken == null) {
throw new ResourceNotFoundException();
}
OAuthTokenInfo accessTokenInfo = new OAuthTokenInfo();
accessTokenInfo.username = username;
accessTokenInfo.resourceHost = hostName;
accessTokenInfo.accessToken = accessToken.getToken();
accessTokenInfo.providerId = accessToken.getProviderId();
accessTokenInfo.expiresAt = Long.toString(accessToken.getExpiresAt());
accessTokenInfo.type = BEARER_TYPE;
return accessTokenInfo;
}
private static String getHostName(String canonicalWebUrl) {
try {
return new URI(canonicalWebUrl).getHost();
} catch (URISyntaxException e) {
return null;
}
}
public static class OAuthTokenInfo {
public String username;
public String resourceHost;
public String accessToken;
public String providerId;
public String expiresAt;
public String type;
}
}

View File

@ -66,6 +66,8 @@ public class Module extends RestApiModule {
get(SSH_KEY_KIND).to(GetSshKey.class);
delete(SSH_KEY_KIND).to(DeleteSshKey.class);
get(ACCOUNT_KIND, "oauthtoken").to(GetOAuthToken.class);
get(ACCOUNT_KIND, "avatar").to(GetAvatar.class);
get(ACCOUNT_KIND, "avatar.change.url").to(GetAvatarChangeUrl.class);

View File

@ -128,6 +128,7 @@ public class GetServerInfo implements RestReadView<ConfigResource> {
info.useContributorAgreements = toBoolean(cfg.isUseContributorAgreements());
info.editableAccountFields = new ArrayList<>(realm.getEditableFields());
info.switchAccountUrl = cfg.getSwitchAccountUrl();
info.isGitBasicAuth = toBoolean(cfg.isGitBasicAuth());
switch (info.authType) {
case LDAP:
@ -135,7 +136,6 @@ public class GetServerInfo implements RestReadView<ConfigResource> {
info.registerUrl = cfg.getRegisterUrl();
info.registerText = cfg.getRegisterText();
info.editFullNameUrl = cfg.getEditFullNameUrl();
info.isGitBasicAuth = toBoolean(cfg.isGitBasicAuth());
break;
case CUSTOM_EXTENSION: