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
Этот коммит содержится в:
коммит произвёл
Joram Wilander
родитель
e406a92fbb
Коммит
9ab5a79962
2
Makefile
2
Makefile
@@ -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
|
||||||
|
|||||||
69
api/oauth.go
69
api/oauth.go
@@ -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 {
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user