PLT-3745 - Deauthorize OAuth Apps (#3852)

* Deauthorize OAuth APIs

* Deautorize OAuth Apps Account Settings

* Fix typo in client method

* Fix issues found by PM

* Show help text only when there is at least one authorized app
Этот коммит содержится в:
enahum
2016-08-23 19:06:17 -03:00
коммит произвёл Joram Wilander
родитель e406a92fbb
Коммит 9ab5a79962
12 изменённых файлов: 532 добавлений и 41 удалений

Просмотреть файл

@@ -159,7 +159,7 @@ test-server: start-docker prepare-enterprise
rm -f cover.out rm -f cover.out
echo "mode: count" > cover.out echo "mode: count" > cover.out
$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=440s -covermode=count -coverprofile=capi.out ./api || exit 1 $(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=650s -covermode=count -coverprofile=capi.out ./api || exit 1
$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=60s -covermode=count -coverprofile=cmodel.out ./model || exit 1 $(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=60s -covermode=count -coverprofile=cmodel.out ./model || exit 1
$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=180s -covermode=count -coverprofile=cstore.out ./store || exit 1 $(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=180s -covermode=count -coverprofile=cstore.out ./store || exit 1
$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=120s -covermode=count -coverprofile=cutils.out ./utils || exit 1 $(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=120s -covermode=count -coverprofile=cutils.out ./utils || exit 1

Просмотреть файл

@@ -29,7 +29,9 @@ func InitOAuth() {
BaseRoutes.OAuth.Handle("/list", ApiUserRequired(getOAuthApps)).Methods("GET") BaseRoutes.OAuth.Handle("/list", ApiUserRequired(getOAuthApps)).Methods("GET")
BaseRoutes.OAuth.Handle("/app/{client_id}", ApiUserRequired(getOAuthAppInfo)).Methods("GET") BaseRoutes.OAuth.Handle("/app/{client_id}", ApiUserRequired(getOAuthAppInfo)).Methods("GET")
BaseRoutes.OAuth.Handle("/allow", ApiUserRequired(allowOAuth)).Methods("GET") BaseRoutes.OAuth.Handle("/allow", ApiUserRequired(allowOAuth)).Methods("GET")
BaseRoutes.OAuth.Handle("/authorized", ApiUserRequired(getAuthorizedApps)).Methods("GET")
BaseRoutes.OAuth.Handle("/delete", ApiUserRequired(deleteOAuthApp)).Methods("POST") BaseRoutes.OAuth.Handle("/delete", ApiUserRequired(deleteOAuthApp)).Methods("POST")
BaseRoutes.OAuth.Handle("/{id:[A-Za-z0-9]+}/deauthorize", AppHandlerIndependent(deauthorizeOAuthApp)).Methods("POST")
BaseRoutes.OAuth.Handle("/{service:[A-Za-z0-9]+}/complete", AppHandlerIndependent(completeOAuth)).Methods("GET") BaseRoutes.OAuth.Handle("/{service:[A-Za-z0-9]+}/complete", AppHandlerIndependent(completeOAuth)).Methods("GET")
BaseRoutes.OAuth.Handle("/{service:[A-Za-z0-9]+}/login", AppHandlerIndependent(loginWithOAuth)).Methods("GET") BaseRoutes.OAuth.Handle("/{service:[A-Za-z0-9]+}/login", AppHandlerIndependent(loginWithOAuth)).Methods("GET")
BaseRoutes.OAuth.Handle("/{service:[A-Za-z0-9]+}/signup", AppHandlerIndependent(signupWithOAuth)).Methods("GET") BaseRoutes.OAuth.Handle("/{service:[A-Za-z0-9]+}/signup", AppHandlerIndependent(signupWithOAuth)).Methods("GET")
@@ -227,6 +229,28 @@ func allowOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
w.Write([]byte(model.MapToJson(responseData))) w.Write([]byte(model.MapToJson(responseData)))
} }
func getAuthorizedApps(c *Context, w http.ResponseWriter, r *http.Request) {
if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider {
c.Err = model.NewLocAppError("getAuthorizedApps", "api.oauth.allow_oauth.turn_off.app_error", nil, "")
c.Err.StatusCode = http.StatusNotImplemented
return
}
ochan := Srv.Store.OAuth().GetAuthorizedApps(c.Session.UserId)
if result := <-ochan; result.Err != nil {
c.Err = result.Err
return
} else {
apps := result.Data.([]*model.OAuthApp)
for k, a := range apps {
a.Sanitize()
apps[k] = a
}
w.Write([]byte(model.OAuthAppListToJson(apps)))
}
}
func RevokeAccessToken(token string) *model.AppError { func RevokeAccessToken(token string) *model.AppError {
schan := Srv.Store.Session().Remove(token) schan := Srv.Store.Session().Remove(token)
@@ -879,6 +903,51 @@ func deleteOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) {
ReturnStatusOK(w) ReturnStatusOK(w)
} }
func deauthorizeOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) {
if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider {
c.Err = model.NewLocAppError("deleteOAuthApp", "api.oauth.allow_oauth.turn_off.app_error", nil, "")
c.Err.StatusCode = http.StatusNotImplemented
return
}
params := mux.Vars(r)
id := params["id"]
if len(id) == 0 {
c.SetInvalidParam("deauthorizeOAuthApp", "id")
return
}
// revoke app sessions
if result := <-Srv.Store.OAuth().GetAccessDataByUserForApp(c.Session.UserId, id); result.Err != nil {
c.Err = result.Err
return
} else {
accessData := result.Data.([]*model.AccessData)
for _, a := range accessData {
if err := RevokeAccessToken(a.Token); err != nil {
c.Err = err
return
}
if rad := <-Srv.Store.OAuth().RemoveAccessData(a.Token); rad.Err != nil {
c.Err = rad.Err
return
}
}
}
// Deauthorize the app
if err := (<-Srv.Store.Preference().Delete(c.Session.UserId, model.PREFERENCE_CATEGORY_AUTHORIZED_OAUTH_APP, id)).Err; err != nil {
c.Err = err
return
}
c.LogAudit("success")
ReturnStatusOK(w)
}
func newSession(appName string, user *model.User) (*model.Session, *model.AppError) { func newSession(appName string, user *model.User) (*model.Session, *model.AppError) {
// set new token an session // set new token an session
session := &model.Session{UserId: user.Id, Roles: user.Roles, IsOAuth: true} session := &model.Session{UserId: user.Id, Roles: user.Roles, IsOAuth: true}

Просмотреть файл

@@ -222,6 +222,62 @@ func TestGetOAuthAppInfo(t *testing.T) {
} }
} }
func TestGetAuthorizedApps(t *testing.T) {
th := Setup().InitBasic().InitSystemAdmin()
Client := th.BasicClient
AdminClient := th.SystemAdminClient
utils.Cfg.ServiceSettings.EnableOAuthServiceProvider = true
app := &model.OAuthApp{Name: "TestApp5" + model.NewId(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}}
app = AdminClient.Must(AdminClient.RegisterApp(app)).Data.(*model.OAuthApp)
if _, err := Client.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, app.Id, "https://nowhere.com", "user", ""); err != nil {
t.Fatal(err)
}
if result, err := Client.GetOAuthAuthorizedApps(); err != nil {
t.Fatal(err)
} else {
apps := result.Data.([]*model.OAuthApp)
if len(apps) != 1 {
t.Fatal("incorrect number of apps should have been 1")
}
}
}
func TestDeauthorizeApp(t *testing.T) {
th := Setup().InitBasic().InitSystemAdmin()
Client := th.BasicClient
AdminClient := th.SystemAdminClient
utils.Cfg.ServiceSettings.EnableOAuthServiceProvider = true
app := &model.OAuthApp{Name: "TestApp5" + model.NewId(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}}
app = AdminClient.Must(AdminClient.RegisterApp(app)).Data.(*model.OAuthApp)
if _, err := Client.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, app.Id, "https://nowhere.com", "user", ""); err != nil {
t.Fatal(err)
}
if err := Client.OAuthDeauthorizeApp(app.Id); err != nil {
t.Fatal(err)
}
if result, err := Client.GetOAuthAuthorizedApps(); err != nil {
t.Fatal(err)
} else {
apps := result.Data.([]*model.OAuthApp)
if len(apps) != 0 {
t.Fatal("incorrect number of apps should have been 0")
}
}
}
func TestOAuthDeleteApp(t *testing.T) { func TestOAuthDeleteApp(t *testing.T) {
th := Setup().InitBasic().InitSystemAdmin() th := Setup().InitBasic().InitSystemAdmin()
Client := th.BasicClient Client := th.BasicClient

Просмотреть файл

@@ -3931,6 +3931,10 @@
"id": "store.sql_oauth.get_access_data.app_error", "id": "store.sql_oauth.get_access_data.app_error",
"translation": "We encountered an error finding the access token" "translation": "We encountered an error finding the access token"
}, },
{
"id": "store.sql_oauth.get_access_data_by_user_for_app.app_error",
"translation": "We encountered an error finding all the access tokens"
},
{ {
"id": "store.sql_oauth.get_app.find.app_error", "id": "store.sql_oauth.get_app.find.app_error",
"translation": "We couldn't find the requested app" "translation": "We couldn't find the requested app"

Просмотреть файл

@@ -1532,6 +1532,29 @@ func (c *Client) DeleteOAuthApp(id string) (*Result, *AppError) {
} }
} }
// GetOAuthAuthorizedApps returns the OAuth2 Apps authorized by the user. On success
// it returns a list of sanitized OAuth2 Authorized Apps by the user.
func (c *Client) GetOAuthAuthorizedApps() (*Result, *AppError) {
if r, err := c.DoApiGet("/oauth/authorized", "", ""); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), OAuthAppListFromJson(r.Body)}, nil
}
}
// OAuthDeauthorizeApp deauthorize a user an OAuth 2.0 app. On success
// it returns status OK or an AppError on fail.
func (c *Client) OAuthDeauthorizeApp(clientId string) *AppError {
if r, err := c.DoApiPost("/oauth/"+clientId+"/deauthorize", ""); err != nil {
return err
} else {
defer closeBody(r)
return nil
}
}
func (c *Client) GetAccessToken(data url.Values) (*Result, *AppError) { func (c *Client) GetAccessToken(data url.Values) (*Result, *AppError) {
if r, err := c.DoPost("/oauth/access_token", data.Encode(), "application/x-www-form-urlencoded"); err != nil { if r, err := c.DoPost("/oauth/access_token", data.Encode(), "application/x-www-form-urlencoded"); err != nil {
return nil, err return nil, err

Просмотреть файл

@@ -211,6 +211,29 @@ func (as SqlOAuthStore) GetApps() StoreChannel {
return storeChannel return storeChannel
} }
func (as SqlOAuthStore) GetAuthorizedApps(userId string) StoreChannel {
storeChannel := make(StoreChannel)
go func() {
result := StoreResult{}
var apps []*model.OAuthApp
if _, err := as.GetReplica().Select(&apps,
`SELECT o.* FROM OAuthApps AS o INNER JOIN
Preferences AS p ON p.Name=o.Id AND p.UserId=:UserId`, map[string]interface{}{"UserId": userId}); err != nil {
result.Err = model.NewLocAppError("SqlOAuthStore.GetAuthorizedApps", "store.sql_oauth.get_apps.find.app_error", nil, "err="+err.Error())
}
result.Data = apps
storeChannel <- result
close(storeChannel)
}()
return storeChannel
}
func (as SqlOAuthStore) DeleteApp(id string) StoreChannel { func (as SqlOAuthStore) DeleteApp(id string) StoreChannel {
storeChannel := make(StoreChannel) storeChannel := make(StoreChannel)
@@ -294,6 +317,33 @@ func (as SqlOAuthStore) GetAccessData(token string) StoreChannel {
return storeChannel return storeChannel
} }
func (as SqlOAuthStore) GetAccessDataByUserForApp(userId, clientId string) StoreChannel {
storeChannel := make(StoreChannel)
go func() {
result := StoreResult{}
var accessData []*model.AccessData
if _, err := as.GetReplica().Select(&accessData,
"SELECT * FROM OAuthAccessData WHERE UserId = :UserId AND ClientId = :ClientId",
map[string]interface{}{"UserId": userId, "ClientId": clientId}); err != nil {
result.Err = model.NewLocAppError("SqlOAuthStore.GetAccessDataByUserForApp",
"store.sql_oauth.get_access_data_by_user_for_app.app_error", nil,
"user_id="+userId+" client_id="+clientId)
} else {
result.Data = accessData
}
storeChannel <- result
close(storeChannel)
}()
return storeChannel
}
func (as SqlOAuthStore) GetAccessDataByRefreshToken(token string) StoreChannel { func (as SqlOAuthStore) GetAccessDataByRefreshToken(token string) StoreChannel {
storeChannel := make(StoreChannel) storeChannel := make(StoreChannel)

Просмотреть файл

@@ -202,6 +202,82 @@ func TestOAuthStoreRemoveAuthDataByUser(t *testing.T) {
} }
} }
func TestOAuthGetAuthorizedApps(t *testing.T) {
Setup()
a1 := model.OAuthApp{}
a1.CreatorId = model.NewId()
a1.Name = "TestApp" + model.NewId()
a1.CallbackUrls = []string{"https://nowhere.com"}
a1.Homepage = "https://nowhere.com"
Must(store.OAuth().SaveApp(&a1))
// allow the app
p := model.Preference{}
p.UserId = a1.CreatorId
p.Category = model.PREFERENCE_CATEGORY_AUTHORIZED_OAUTH_APP
p.Name = a1.Id
p.Value = "true"
Must(store.Preference().Save(&model.Preferences{p}))
if result := <-store.OAuth().GetAuthorizedApps(a1.CreatorId); result.Err != nil {
t.Fatal(result.Err)
} else {
apps := result.Data.([]*model.OAuthApp)
if len(apps) == 0 {
t.Fatal("It should have return apps")
}
}
}
func TestOAuthGetAccessDataByUserForApp(t *testing.T) {
Setup()
a1 := model.OAuthApp{}
a1.CreatorId = model.NewId()
a1.Name = "TestApp" + model.NewId()
a1.CallbackUrls = []string{"https://nowhere.com"}
a1.Homepage = "https://nowhere.com"
Must(store.OAuth().SaveApp(&a1))
// allow the app
p := model.Preference{}
p.UserId = a1.CreatorId
p.Category = model.PREFERENCE_CATEGORY_AUTHORIZED_OAUTH_APP
p.Name = a1.Id
p.Value = "true"
Must(store.Preference().Save(&model.Preferences{p}))
if result := <-store.OAuth().GetAuthorizedApps(a1.CreatorId); result.Err != nil {
t.Fatal(result.Err)
} else {
apps := result.Data.([]*model.OAuthApp)
if len(apps) == 0 {
t.Fatal("It should have return apps")
}
}
// save the token
ad1 := model.AccessData{}
ad1.ClientId = a1.Id
ad1.UserId = a1.CreatorId
ad1.Token = model.NewId()
ad1.RefreshToken = model.NewId()
if err := (<-store.OAuth().SaveAccessData(&ad1)).Err; err != nil {
t.Fatal(err)
}
if result := <-store.OAuth().GetAccessDataByUserForApp(a1.CreatorId, a1.Id); result.Err != nil {
t.Fatal(result.Err)
} else {
accessData := result.Data.([]*model.AccessData)
if len(accessData) == 0 {
t.Fatal("It should have return access data")
}
}
}
func TestOAuthStoreDeleteApp(t *testing.T) { func TestOAuthStoreDeleteApp(t *testing.T) {
a1 := model.OAuthApp{} a1 := model.OAuthApp{}
a1.CreatorId = model.NewId() a1.CreatorId = model.NewId()

Просмотреть файл

@@ -188,6 +188,7 @@ type OAuthStore interface {
GetApp(id string) StoreChannel GetApp(id string) StoreChannel
GetAppByUser(userId string) StoreChannel GetAppByUser(userId string) StoreChannel
GetApps() StoreChannel GetApps() StoreChannel
GetAuthorizedApps(userId string) StoreChannel
DeleteApp(id string) StoreChannel DeleteApp(id string) StoreChannel
SaveAuthData(authData *model.AuthData) StoreChannel SaveAuthData(authData *model.AuthData) StoreChannel
GetAuthData(code string) StoreChannel GetAuthData(code string) StoreChannel
@@ -196,6 +197,7 @@ type OAuthStore interface {
SaveAccessData(accessData *model.AccessData) StoreChannel SaveAccessData(accessData *model.AccessData) StoreChannel
UpdateAccessData(accessData *model.AccessData) StoreChannel UpdateAccessData(accessData *model.AccessData) StoreChannel
GetAccessData(token string) StoreChannel GetAccessData(token string) StoreChannel
GetAccessDataByUserForApp(userId, clientId string) StoreChannel
GetAccessDataByRefreshToken(token string) StoreChannel GetAccessDataByRefreshToken(token string) StoreChannel
GetPreviousAccessData(userId, clientId string) StoreChannel GetPreviousAccessData(userId, clientId string) StoreChannel
RemoveAccessData(token string) StoreChannel RemoveAccessData(token string) StoreChannel

Просмотреть файл

@@ -1553,6 +1553,26 @@ export default class Client {
end(this.handleResponse.bind(this, 'getOAuthAppInfo', success, error)); end(this.handleResponse.bind(this, 'getOAuthAppInfo', success, error));
} }
getAuthorizedApps(success, error) {
request.
get(`${this.getOAuthRoute()}/authorized`).
set(this.defaultHeaders).
type('application/json').
accept('application/json').
send().
end(this.handleResponse.bind(this, 'getAuthorizedApps', success, error));
}
deauthorizeOAuthApp(id, success, error) {
request.
post(`${this.getOAuthRoute()}/${id}/deauthorize`).
set(this.defaultHeaders).
type('application/json').
accept('application/json').
send().
end(this.handleResponse.bind(this, 'deauthorizeOAuthApp', success, error));
}
// Routes for Hooks // Routes for Hooks
addIncomingHook(hook, success, error) { addIncomingHook(hook, success, error) {

Просмотреть файл

@@ -16,33 +16,12 @@ import Constants from 'utils/constants.jsx';
import $ from 'jquery'; import $ from 'jquery';
import React from 'react'; import React from 'react';
import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedHTMLMessage, FormattedTime, FormattedDate} from 'react-intl'; import {FormattedMessage, FormattedHTMLMessage, FormattedTime, FormattedDate} from 'react-intl';
import {Link} from 'react-router/es6'; import {Link} from 'react-router/es6';
const holders = defineMessages({ import icon50 from 'images/icon50x50.png';
currentPasswordError: {
id: 'user.settings.security.currentPasswordError',
defaultMessage: 'Please enter your current password.'
},
passwordLengthError: {
id: 'user.settings.security.passwordLengthError',
defaultMessage: 'New passwords must be at least {min} characters and at most {max} characters.'
},
passwordMatchError: {
id: 'user.settings.security.passwordMatchError',
defaultMessage: 'The new passwords you entered do not match.'
},
method: {
id: 'user.settings.security.method',
defaultMessage: 'Sign-in Method'
},
close: {
id: 'user.settings.security.close',
defaultMessage: 'Close'
}
});
class SecurityTab extends React.Component { export default class SecurityTab extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
@@ -56,7 +35,9 @@ class SecurityTab extends React.Component {
this.getDefaultState = this.getDefaultState.bind(this); this.getDefaultState = this.getDefaultState.bind(this);
this.createPasswordSection = this.createPasswordSection.bind(this); this.createPasswordSection = this.createPasswordSection.bind(this);
this.createSignInSection = this.createSignInSection.bind(this); this.createSignInSection = this.createSignInSection.bind(this);
this.createOAuthAppsSection = this.createOAuthAppsSection.bind(this);
this.showQrCode = this.showQrCode.bind(this); this.showQrCode = this.showQrCode.bind(this);
this.deauthorizeApp = this.deauthorizeApp.bind(this);
this.state = this.getDefaultState(); this.state = this.getDefaultState();
} }
@@ -74,6 +55,16 @@ class SecurityTab extends React.Component {
}; };
} }
componentDidMount() {
Client.getAuthorizedApps(
(authorizedApps) => {
this.setState({authorizedApps, serverError: null}); //eslint-disable-line react/no-did-mount-set-state
},
(err) => {
this.setState({serverError: err.message}); //eslint-disable-line react/no-did-mount-set-state
});
}
submitPassword(e) { submitPassword(e) {
e.preventDefault(); e.preventDefault();
@@ -82,9 +73,8 @@ class SecurityTab extends React.Component {
var newPassword = this.state.newPassword; var newPassword = this.state.newPassword;
var confirmPassword = this.state.confirmPassword; var confirmPassword = this.state.confirmPassword;
const {formatMessage} = this.props.intl;
if (currentPassword === '') { if (currentPassword === '') {
this.setState({passwordError: formatMessage(holders.currentPasswordError), serverError: ''}); this.setState({passwordError: Utils.localizeMessage('user.settings.security.currentPasswordError', 'Please enter your current password.'), serverError: ''});
return; return;
} }
@@ -98,7 +88,7 @@ class SecurityTab extends React.Component {
} }
if (newPassword !== confirmPassword) { if (newPassword !== confirmPassword) {
var defaultState = Object.assign(this.getDefaultState(), {passwordError: formatMessage(holders.passwordMatchError), serverError: ''}); var defaultState = Object.assign(this.getDefaultState(), {passwordError: Utils.localizeMessage('user.settings.security.passwordMatchError', 'The new passwords you entered do not match.'), serverError: ''});
this.setState(defaultState); this.setState(defaultState);
return; return;
} }
@@ -190,6 +180,23 @@ class SecurityTab extends React.Component {
this.setState({mfaShowQr: true}); this.setState({mfaShowQr: true});
} }
deauthorizeApp(e) {
e.preventDefault();
const appId = e.currentTarget.getAttribute('data-app');
Client.deauthorizeOAuthApp(
appId,
() => {
const authorizedApps = this.state.authorizedApps.filter((app) => {
return app.id !== appId;
});
this.setState({authorizedApps, serverError: null});
},
(err) => {
this.setState({serverError: err.message});
});
}
createMfaSection() { createMfaSection() {
let updateSectionStatus; let updateSectionStatus;
let submit; let submit;
@@ -686,7 +693,7 @@ class SecurityTab extends React.Component {
return ( return (
<SettingItemMax <SettingItemMax
title={this.props.intl.formatMessage(holders.method)} title={Utils.localizeMessage('user.settings.security.method', 'Sign-in Method')}
extraInfo={extraInfo} extraInfo={extraInfo}
inputs={inputs} inputs={inputs}
server_error={this.state.serverError} server_error={this.state.serverError}
@@ -744,36 +751,180 @@ class SecurityTab extends React.Component {
return ( return (
<SettingItemMin <SettingItemMin
title={this.props.intl.formatMessage(holders.method)} title={Utils.localizeMessage('user.settings.security.method', 'Sign-in Method')}
describe={describe} describe={describe}
updateSection={updateSectionStatus} updateSection={updateSectionStatus}
/> />
); );
} }
createOAuthAppsSection() {
let updateSectionStatus;
if (this.props.activeSection === 'apps') {
let apps;
if (this.state.authorizedApps && this.state.authorizedApps.length > 0) {
apps = this.state.authorizedApps.map((app) => {
const homepage = (
<a
href={app.homepage}
target='_blank'
rel='noopener noreferrer'
>
{app.homepage}
</a>
);
return (
<div
key={app.id}
className='padding-bottom x2 authorized-app'
>
<div className='col-sm-10'>
<div className='authorized-app__name'>
{app.name}
<span className='authorized-app__url'>
{' -'} {homepage}
</span>
</div>
<div className='authorized-app__description'>{app.description}</div>
<div className='authorized-app__deauthorize'>
<a
href='#'
data-app={app.id}
onClick={this.deauthorizeApp}
>
<FormattedMessage
id='user.settings.security.deauthorize'
defaultMessage='Deauthorize'
/>
</a>
</div>
</div>
<div className='col-sm-2 pull-right'>
<img
alt={app.name}
src={app.icon_url || icon50}
/>
</div>
<br/>
</div>
);
});
} else {
apps = (
<div className='padding-bottom x2 authorized-app'>
<div className='col-sm-12'>
<div className='setting-list__hint'>
<FormattedMessage
id='user.settings.security.noApps'
defaultMessage='No OAuth 2.0 Applications are authorized.'
/>
</div>
</div>
</div>
);
}
const inputs = [];
let wrapperClass;
let helpText;
if (Array.isArray(apps)) {
wrapperClass = 'authorized-apps__wrapper';
helpText = (
<div className='authorized-apps__help'>
<FormattedMessage
id='user.settings.security.oauthAppsHelp'
defaultMessage='Applications act on your behalf to access your data based on the permissions you grant them.'
/>
</div>
);
}
inputs.push(
<div
className={wrapperClass}
key='authorizedApps'
>
{apps}
</div>
);
updateSectionStatus = function updateSection(e) {
this.props.updateSection('');
this.setState({serverError: null});
e.preventDefault();
}.bind(this);
const title = (
<div>
<FormattedMessage
id='user.settings.security.oauthApps'
defaultMessage='OAuth 2.0 Applications'
/>
{helpText}
</div>
);
return (
<SettingItemMax
title={title}
inputs={inputs}
server_error={this.state.serverError}
updateSection={updateSectionStatus}
width='full'
/>
);
}
updateSectionStatus = function updateSection() {
this.props.updateSection('apps');
}.bind(this);
return (
<SettingItemMin
title={Utils.localizeMessage('user.settings.security.oauthApps', 'OAuth 2.0 Applications')}
describe={
<FormattedMessage
id='user.settings.security.oauthAppsDescription'
defaultMessage="Click 'Edit' to manage your OAuth 2.0 Applications"
/>
}
updateSection={updateSectionStatus}
/>
);
}
render() { render() {
const user = this.props.user; const user = this.props.user;
const config = window.mm_config;
const passwordSection = this.createPasswordSection(); const passwordSection = this.createPasswordSection();
let numMethods = 0; let numMethods = 0;
numMethods = global.window.mm_config.EnableSignUpWithGitLab === 'true' ? numMethods + 1 : numMethods; numMethods = config.EnableSignUpWithGitLab === 'true' ? numMethods + 1 : numMethods;
numMethods = global.window.mm_config.EnableSignUpWithGoogle === 'true' ? numMethods + 1 : numMethods; numMethods = config.EnableSignUpWithGoogle === 'true' ? numMethods + 1 : numMethods;
numMethods = global.window.mm_config.EnableLdap === 'true' ? numMethods + 1 : numMethods; numMethods = config.EnableLdap === 'true' ? numMethods + 1 : numMethods;
numMethods = global.window.mm_config.EnableSaml === 'true' ? numMethods + 1 : numMethods; numMethods = config.EnableSaml === 'true' ? numMethods + 1 : numMethods;
let signInSection; let signInSection;
if (global.window.mm_config.EnableSignUpWithEmail === 'true' && numMethods > 0) { if (config.EnableSignUpWithEmail === 'true' && numMethods > 0) {
signInSection = this.createSignInSection(); signInSection = this.createSignInSection();
} }
let mfaSection; let mfaSection;
if (global.window.mm_config.EnableMultifactorAuthentication === 'true' && if (config.EnableMultifactorAuthentication === 'true' &&
global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.IsLicensed === 'true' &&
(user.auth_service === '' || user.auth_service === Constants.LDAP_SERVICE)) { (user.auth_service === '' || user.auth_service === Constants.LDAP_SERVICE)) {
mfaSection = this.createMfaSection(); mfaSection = this.createMfaSection();
} }
let oauthSection;
if (config.EnableOAuthServiceProvider === 'true') {
oauthSection = this.createOAuthAppsSection();
}
return ( return (
<div> <div>
<div className='modal-header'> <div className='modal-header'>
@@ -781,7 +932,7 @@ class SecurityTab extends React.Component {
type='button' type='button'
className='close' className='close'
data-dismiss='modal' data-dismiss='modal'
aria-label={this.props.intl.formatMessage(holders.close)} aria-label={Utils.localizeMessage('user.settings.security.close', 'Close')}
onClick={this.props.closeModal} onClick={this.props.closeModal}
> >
<span aria-hidden='true'>{'×'}</span> <span aria-hidden='true'>{'×'}</span>
@@ -814,6 +965,8 @@ class SecurityTab extends React.Component {
<div className='divider-light'/> <div className='divider-light'/>
{mfaSection} {mfaSection}
<div className='divider-light'/> <div className='divider-light'/>
{oauthSection}
<div className='divider-light'/>
{signInSection} {signInSection}
<div className='divider-dark'/> <div className='divider-dark'/>
<br></br> <br></br>
@@ -849,7 +1002,6 @@ SecurityTab.defaultProps = {
activeSection: '' activeSection: ''
}; };
SecurityTab.propTypes = { SecurityTab.propTypes = {
intl: intlShape.isRequired,
user: React.PropTypes.object, user: React.PropTypes.object,
activeSection: React.PropTypes.string, activeSection: React.PropTypes.string,
updateSection: React.PropTypes.func, updateSection: React.PropTypes.func,
@@ -858,5 +1010,3 @@ SecurityTab.propTypes = {
collapseModal: React.PropTypes.func.isRequired, collapseModal: React.PropTypes.func.isRequired,
setEnforceFocus: React.PropTypes.func.isRequired setEnforceFocus: React.PropTypes.func.isRequired
}; };
export default injectIntl(SecurityTab);

Просмотреть файл

@@ -1961,5 +1961,10 @@
"web.footer.terms": "Terms", "web.footer.terms": "Terms",
"web.header.back": "Back", "web.header.back": "Back",
"web.root.signup_info": "All team communication in one place, searchable and accessible anywhere", "web.root.signup_info": "All team communication in one place, searchable and accessible anywhere",
"youtube_video.notFound": "Video not found" "youtube_video.notFound": "Video not found",
"user.settings.security.deauthorize": "Deauthorize",
"user.settings.security.noApps": "No OAuth 2.0 Applications are authorized.",
"user.settings.security.oauthApps": "OAuth 2.0 Applications",
"user.settings.security.oauthAppsDescription": "Click 'Edit' to manage your OAuth 2.0 Applications",
"user.settings.security.oauthAppsHelp": "Applications act on your behalf to access your data based on the permissions you grant them."
} }

Просмотреть файл

@@ -7,6 +7,42 @@
max-height: 300px; max-height: 300px;
max-width: 560px; max-width: 560px;
} }
.authorized-apps__help {
font-size: 13px;
font-weight: 400;
margin-top: 7px;
}
.authorized-apps__wrapper {
background-color: #fff;
padding: 10px 0;
}
.authorized-app {
display: inline-block;
width: 100%;
&:not(:last-child) {
border-bottom: 1px solid #ccc;
margin-bottom: 10px;
}
.authorized-app__name {
font-weight: 600;
}
.authorized-app__url {
font-size: 13px;
font-weight: 400;
}
.authorized-app__description,
.authorized-app__deauthorize {
font-size: 13px;
margin: 5px 0;
}
}
} }
.modal { .modal {