feat(ACI): add UAC to ACI

pull/4952/head
Felix Han 2021-03-30 10:58:56 +13:00
parent ad2910f3f0
commit e3e7e84821
18 changed files with 410 additions and 18 deletions

View File

@ -78,6 +78,8 @@ func (handler *Handler) resourceControlCreate(w http.ResponseWriter, r *http.Req
switch payload.Type { switch payload.Type {
case "container": case "container":
resourceControlType = portainer.ContainerResourceControl resourceControlType = portainer.ContainerResourceControl
case "container-group":
resourceControlType = portainer.ContainerGroupResourceControl
case "service": case "service":
resourceControlType = portainer.ServiceResourceControl resourceControlType = portainer.ServiceResourceControl
case "volume": case "volume":

View File

@ -8,13 +8,13 @@ import (
"github.com/portainer/portainer/api/http/proxy/factory/azure" "github.com/portainer/portainer/api/http/proxy/factory/azure"
) )
func newAzureProxy(endpoint *portainer.Endpoint) (http.Handler, error) { func newAzureProxy(endpoint *portainer.Endpoint, dataStore portainer.DataStore) (http.Handler, error) {
remoteURL, err := url.Parse(azureAPIBaseURL) remoteURL, err := url.Parse(azureAPIBaseURL)
if err != nil { if err != nil {
return nil, err return nil, err
} }
proxy := newSingleHostReverseProxyWithHostHeader(remoteURL) proxy := newSingleHostReverseProxyWithHostHeader(remoteURL)
proxy.Transport = azure.NewTransport(&endpoint.AzureCredentials) proxy.Transport = azure.NewTransport(&endpoint.AzureCredentials, dataStore, endpoint)
return proxy, nil return proxy, nil
} }

View File

@ -0,0 +1,149 @@
package azure
import (
"log"
"net/http"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/authorization"
)
func (transport *Transport) createAzureRequestContext(request *http.Request) (*azureRequestContext, error) {
var err error
tokenData, err := security.RetrieveTokenData(request)
if err != nil {
return nil, err
}
resourceControls, err := transport.dataStore.ResourceControl().ResourceControls()
if err != nil {
return nil, err
}
context := &azureRequestContext{
isAdmin: true,
userID: tokenData.ID,
resourceControls: resourceControls,
}
if tokenData.Role != portainer.AdministratorRole {
context.isAdmin = false
teamMemberships, err := transport.dataStore.TeamMembership().TeamMembershipsByUserID(tokenData.ID)
if err != nil {
return nil, err
}
userTeamIDs := make([]portainer.TeamID, 0)
for _, membership := range teamMemberships {
userTeamIDs = append(userTeamIDs, membership.TeamID)
}
context.userTeamIDs = userTeamIDs
}
return context, nil
}
func decorateObject(object map[string]interface{}, resourceControl *portainer.ResourceControl) map[string]interface{} {
if object["Portainer"] == nil {
object["Portainer"] = make(map[string]interface{})
}
portainerMetadata := object["Portainer"].(map[string]interface{})
portainerMetadata["ResourceControl"] = resourceControl
return object
}
func (transport *Transport) createPrivateResourceControl(
resourceIdentifier string,
resourceType portainer.ResourceControlType,
userID portainer.UserID) (*portainer.ResourceControl, error) {
resourceControl := authorization.NewPrivateResourceControl(resourceIdentifier, resourceType, userID)
err := transport.dataStore.ResourceControl().CreateResourceControl(resourceControl)
if err != nil {
log.Printf("[ERROR] [http,proxy,azure,transport] [message: unable to persist resource control] [resource: %s] [err: %s]", resourceIdentifier, err)
return nil, err
}
return resourceControl, nil
}
func (transport *Transport) userCanDeleteContainerGroup(request *http.Request, context *azureRequestContext) bool {
if context.isAdmin {
return true
}
resourceIdentifier := request.URL.Path
resourceControl := transport.findResourceControl(resourceIdentifier, context)
return authorization.UserCanAccessResource(context.userID, context.userTeamIDs, resourceControl)
}
func (transport *Transport) decorateContainerGroups(containerGroups []interface{}, context *azureRequestContext) []interface{} {
decoratedContainerGroups := make([]interface{}, 0)
for _, containerGroup := range containerGroups {
containerGroup = transport.decorateContainerGroup(containerGroup.(map[string]interface{}), context)
decoratedContainerGroups = append(decoratedContainerGroups, containerGroup)
}
return decoratedContainerGroups
}
func (transport *Transport) decorateContainerGroup(containerGroup map[string]interface{}, context *azureRequestContext) map[string]interface{} {
containerGroupId, ok := containerGroup["id"].(string)
if ok {
resourceControl := transport.findResourceControl(containerGroupId, context)
if resourceControl != nil {
containerGroup = decorateObject(containerGroup, resourceControl)
}
} else {
log.Printf("[WARN] [http,proxy,azure,decorate] [message: unable to find resource id property in container group]")
}
return containerGroup
}
func (transport *Transport) filterContainerGroups(containerGroups []interface{}, context *azureRequestContext) []interface{} {
filteredContainerGroups := make([]interface{}, 0)
for _, containerGroup := range containerGroups {
userCanAccessResource := false
containerGroup := containerGroup.(map[string]interface{})
portainerObject, ok := containerGroup["Portainer"].(map[string]interface{})
if ok {
resourceControl, ok := portainerObject["ResourceControl"].(*portainer.ResourceControl)
if ok {
userCanAccessResource = authorization.UserCanAccessResource(context.userID, context.userTeamIDs, resourceControl)
}
}
if context.isAdmin || userCanAccessResource {
filteredContainerGroups = append(filteredContainerGroups, containerGroup)
}
}
return filteredContainerGroups
}
func (transport *Transport) removeResourceControl(containerGroup map[string]interface{}, context *azureRequestContext) error {
containerGroupID, ok := containerGroup["id"].(string)
if ok {
resourceControl := transport.findResourceControl(containerGroupID, context)
if resourceControl != nil {
err := transport.dataStore.ResourceControl().DeleteResourceControl(resourceControl.ID)
return err
}
} else {
log.Printf("[WARN] [http,proxy,azure] [message: missign ID in container group]")
}
return nil
}
func (transport *Transport) findResourceControl(containerGroupId string, context *azureRequestContext) *portainer.ResourceControl {
resourceControl := authorization.GetResourceControlByResourceIDAndType(containerGroupId, portainer.ContainerGroupResourceControl, context.resourceControls)
return resourceControl
}

View File

@ -0,0 +1,109 @@
package azure
import (
"errors"
"net/http"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
)
// proxy for /subscriptions/*/resourceGroups/*/providers/Microsoft.ContainerInstance/containerGroups/*
func (transport *Transport) proxyContainerGroupRequest(request *http.Request) (*http.Response, error) {
switch request.Method {
case http.MethodPut:
return transport.proxyContainerGroupPutRequest(request)
case http.MethodGet:
return transport.proxyContainerGroupGetRequest(request)
case http.MethodDelete:
return transport.proxyContainerGroupDeleteRequest(request)
default:
return http.DefaultTransport.RoundTrip(request)
}
}
func (transport *Transport) proxyContainerGroupPutRequest(request *http.Request) (*http.Response, error) {
response, err := http.DefaultTransport.RoundTrip(request)
if err != nil {
return response, err
}
responseObject, err := responseutils.GetResponseAsJSONOBject(response)
if err != nil {
return response, err
}
containerGroupID, ok := responseObject["id"].(string)
if !ok {
return response, errors.New("Missing container group ID")
}
context, err := transport.createAzureRequestContext(request)
if err != nil {
return response, err
}
resourceControl, err := transport.createPrivateResourceControl(containerGroupID, portainer.ContainerGroupResourceControl, context.userID)
if err != nil {
return response, err
}
responseObject = decorateObject(responseObject, resourceControl)
err = responseutils.RewriteResponse(response, responseObject, http.StatusOK)
if err != nil {
return response, err
}
return response, nil
}
func (transport *Transport) proxyContainerGroupGetRequest(request *http.Request) (*http.Response, error) {
response, err := http.DefaultTransport.RoundTrip(request)
if err != nil {
return response, err
}
responseObject, err := responseutils.GetResponseAsJSONOBject(response)
if err != nil {
return nil, err
}
context, err := transport.createAzureRequestContext(request)
if err != nil {
return nil, err
}
responseObject = transport.decorateContainerGroup(responseObject, context)
responseutils.RewriteResponse(response, responseObject, http.StatusOK)
return response, nil
}
func (transport *Transport) proxyContainerGroupDeleteRequest(request *http.Request) (*http.Response, error) {
context, err := transport.createAzureRequestContext(request)
if err != nil {
return nil, err
}
if !transport.userCanDeleteContainerGroup(request, context) {
return responseutils.WriteAccessDeniedResponse()
}
response, err := http.DefaultTransport.RoundTrip(request)
if err != nil {
return response, err
}
responseObject, err := responseutils.GetResponseAsJSONOBject(response)
if err != nil {
return nil, err
}
transport.removeResourceControl(responseObject, context)
responseutils.RewriteResponse(response, responseObject, http.StatusOK)
return response, nil
}

View File

@ -0,0 +1,48 @@
package azure
import (
"fmt"
"net/http"
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
)
// proxy for /subscriptions/*/providers/Microsoft.ContainerInstance/containerGroups
func (transport *Transport) proxyContainerGroupsRequest(request *http.Request) (*http.Response, error) {
switch request.Method {
case http.MethodGet:
return transport.proxyContainerGroupsGetRequest(request)
default:
return http.DefaultTransport.RoundTrip(request)
}
}
func (transport *Transport) proxyContainerGroupsGetRequest(request *http.Request) (*http.Response, error) {
response, err := http.DefaultTransport.RoundTrip(request)
if err != nil {
return nil, err
}
responseObject, err := responseutils.GetResponseAsJSONOBject(response)
if err != nil {
return nil, err
}
value, ok := responseObject["value"].([]interface{})
if ok {
context, err := transport.createAzureRequestContext(request)
if err != nil {
return response, err
}
decoratedValue := transport.decorateContainerGroups(value, context)
filteredValue := transport.filterContainerGroups(decoratedValue, context)
responseObject["value"] = filteredValue
responseutils.RewriteResponse(response, responseObject, http.StatusOK)
} else {
return nil, fmt.Errorf("The container groups response has no value property")
}
return response, nil
}

View File

@ -5,7 +5,7 @@ import (
"strconv" "strconv"
"sync" "sync"
"time" "time"
"path"
"github.com/portainer/portainer/api" "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/client" "github.com/portainer/portainer/api/http/client"
) )
@ -21,26 +21,50 @@ type (
client *client.HTTPClient client *client.HTTPClient
token *azureAPIToken token *azureAPIToken
mutex sync.Mutex mutex sync.Mutex
dataStore portainer.DataStore
endpoint *portainer.Endpoint
}
azureRequestContext struct {
isAdmin bool
userID portainer.UserID
userTeamIDs []portainer.TeamID
resourceControls []portainer.ResourceControl
} }
) )
// NewTransport returns a pointer to a new instance of Transport that implements the HTTP Transport // NewTransport returns a pointer to a new instance of Transport that implements the HTTP Transport
// interface for proxying requests to the Azure API. // interface for proxying requests to the Azure API.
func NewTransport(credentials *portainer.AzureCredentials) *Transport { func NewTransport(credentials *portainer.AzureCredentials, dataStore portainer.DataStore, endpoint *portainer.Endpoint) *Transport {
return &Transport{ return &Transport{
credentials: credentials, credentials: credentials,
client: client.NewHTTPClient(), client: client.NewHTTPClient(),
dataStore: dataStore,
endpoint: endpoint,
} }
} }
// RoundTrip is the implementation of the the http.RoundTripper interface // RoundTrip is the implementation of the the http.RoundTripper interface
func (transport *Transport) RoundTrip(request *http.Request) (*http.Response, error) { func (transport *Transport) RoundTrip(request *http.Request) (*http.Response, error) {
return transport.proxyAzureRequest(request)
}
func (transport *Transport) proxyAzureRequest(request *http.Request) (*http.Response, error) {
requestPath := request.URL.Path
err := transport.retrieveAuthenticationToken() err := transport.retrieveAuthenticationToken()
if err != nil { if err != nil {
return nil, err return nil, err
} }
request.Header.Set("Authorization", "Bearer "+transport.token.value) request.Header.Set("Authorization", "Bearer "+transport.token.value)
if match, _ := path.Match(portainer.AzurePathContainerGroups, requestPath); match {
return transport.proxyContainerGroupsRequest(request)
} else if match, _ := path.Match(portainer.AzurePathContainerGroup, requestPath); match {
return transport.proxyContainerGroupRequest(request)
}
return http.DefaultTransport.RoundTrip(request) return http.DefaultTransport.RoundTrip(request)
} }

View File

@ -55,7 +55,7 @@ func (factory *ProxyFactory) NewLegacyExtensionProxy(extensionAPIURL string) (ht
func (factory *ProxyFactory) NewEndpointProxy(endpoint *portainer.Endpoint) (http.Handler, error) { func (factory *ProxyFactory) NewEndpointProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
switch endpoint.Type { switch endpoint.Type {
case portainer.AzureEnvironment: case portainer.AzureEnvironment:
return newAzureProxy(endpoint) return newAzureProxy(endpoint, factory.dataStore)
case portainer.EdgeAgentOnKubernetesEnvironment, portainer.AgentOnKubernetesEnvironment, portainer.KubernetesLocalEnvironment: case portainer.EdgeAgentOnKubernetesEnvironment, portainer.AgentOnKubernetesEnvironment, portainer.KubernetesLocalEnvironment:
return factory.newKubernetesProxy(endpoint) return factory.newKubernetesProxy(endpoint)
} }

View File

@ -2,6 +2,7 @@ package responseutils
import ( import (
"bytes" "bytes"
"compress/gzip"
"encoding/json" "encoding/json"
"errors" "errors"
"io/ioutil" "io/ioutil"
@ -48,13 +49,21 @@ func getResponseBodyAsGenericJSON(response *http.Response) (interface{}, error)
return nil, errors.New("unable to parse response: empty response body") return nil, errors.New("unable to parse response: empty response body")
} }
var data interface{} reader := response.Body
body, err := ioutil.ReadAll(response.Body)
if response.Header.Get("Content-Encoding") == "gzip" {
response.Header.Del("Content-Encoding")
gzipReader, err := gzip.NewReader(response.Body)
if err != nil { if err != nil {
return nil, err return nil, err
} }
reader = gzipReader
}
err = response.Body.Close() defer reader.Close()
var data interface{}
body, err := ioutil.ReadAll(reader)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -160,9 +160,13 @@ func FilterAuthorizedCustomTemplates(customTemplates []portainer.CustomTemplate,
return authorizedTemplates return authorizedTemplates
} }
// UserCanAccessResource will valide that a user has permissions defined in the specified resource control // UserCanAccessResource will valid that a user has permissions defined in the specified resource control
// based on its identifier and the team(s) he is part of. // based on its identifier and the team(s) he is part of.
func UserCanAccessResource(userID portainer.UserID, userTeamIDs []portainer.TeamID, resourceControl *portainer.ResourceControl) bool { func UserCanAccessResource(userID portainer.UserID, userTeamIDs []portainer.TeamID, resourceControl *portainer.ResourceControl) bool {
if resourceControl == nil {
return false
}
for _, authorizedUserAccess := range resourceControl.UserAccesses { for _, authorizedUserAccess := range resourceControl.UserAccesses {
if userID == authorizedUserAccess.UserID { if userID == authorizedUserAccess.UserID {
return true return true

View File

@ -1495,6 +1495,8 @@ const (
ConfigResourceControl ConfigResourceControl
// CustomTemplateResourceControl represents a resource control associated to a custom template // CustomTemplateResourceControl represents a resource control associated to a custom template
CustomTemplateResourceControl CustomTemplateResourceControl
// ContainerGroupResourceControl represents a resource control associated to an Azure container group
ContainerGroupResourceControl
) )
const ( const (
@ -1771,3 +1773,8 @@ const (
EndpointResourcesAccess Authorization = "EndpointResourcesAccess" EndpointResourcesAccess Authorization = "EndpointResourcesAccess"
) )
const (
AzurePathContainerGroups = "/subscriptions/*/providers/Microsoft.ContainerInstance/containerGroups"
AzurePathContainerGroup = "/subscriptions/*/resourceGroups/*/providers/Microsoft.ContainerInstance/containerGroups/*"
)

View File

@ -49,6 +49,13 @@
<th> <th>
Published Ports Published Ports
</th> </th>
<th>
<a ng-click="$ctrl.changeOrderBy('ResourceControl.Ownership')">
Ownership
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ResourceControl.Ownership' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ResourceControl.Ownership' && $ctrl.state.reverseOrder"></i>
</a>
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -70,6 +77,12 @@
</a> </a>
<span ng-if="item.Ports.length == 0">-</span> <span ng-if="item.Ports.length == 0">-</span>
</td> </td>
<td>
<span>
<i ng-class="item.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
{{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = $ctrl.RCO.ADMINISTRATORS }}
</span>
</td>
</tr> </tr>
<tr ng-if="!$ctrl.dataset"> <tr ng-if="!$ctrl.dataset">
<td colspan="3" class="text-center text-muted">Loading...</td> <td colspan="3" class="text-center text-muted">Loading...</td>

View File

@ -2,7 +2,7 @@ angular.module('portainer.azure').component('containergroupsDatatable', {
templateUrl: './containerGroupsDatatable.html', templateUrl: './containerGroupsDatatable.html',
controller: 'GenericDatatableController', controller: 'GenericDatatableController',
bindings: { bindings: {
title: '@', titleText: '@',
titleIcon: '@', titleIcon: '@',
dataset: '<', dataset: '<',
tableKey: '@', tableKey: '@',

View File

@ -1,3 +1,6 @@
import { AccessControlFormData } from 'Portainer/components/accessControlForm/porAccessControlFormModel';
import { ResourceControlViewModel } from 'Portainer/models/resourceControl/resourceControl';
export function ContainerGroupDefaultModel() { export function ContainerGroupDefaultModel() {
this.Location = ''; this.Location = '';
this.OSType = 'Linux'; this.OSType = 'Linux';
@ -13,6 +16,7 @@ export function ContainerGroupDefaultModel() {
]; ];
this.CPU = 1; this.CPU = 1;
this.Memory = 1; this.Memory = 1;
this.AccessControlData = new AccessControlFormData();
} }
export function ContainerGroupViewModel(data) { export function ContainerGroupViewModel(data) {
@ -30,6 +34,10 @@ export function ContainerGroupViewModel(data) {
this.AllocatePublicIP = data.properties.ipAddress.type === 'Public'; this.AllocatePublicIP = data.properties.ipAddress.type === 'Public';
this.CPU = container.properties.resources.requests.cpu; this.CPU = container.properties.resources.requests.cpu;
this.Memory = container.properties.resources.requests.memoryInGB; this.Memory = container.properties.resources.requests.memoryInGB;
if (data.Portainer && data.Portainer.ResourceControl) {
this.ResourceControl = new ResourceControlViewModel(data.Portainer.ResourceControl);
}
} }
export function CreateContainerGroupRequest(model) { export function CreateContainerGroupRequest(model) {

View File

@ -131,4 +131,8 @@
</rd-widget-body> </rd-widget-body>
</rd-widget> </rd-widget>
</div> </div>
<!-- access-control-panel -->
<por-access-control-panel ng-if="$ctrl.container" resource-id="$ctrl.container.Id" resource-control="$ctrl.container.ResourceControl" resource-type="'container-group'">
</por-access-control-panel>
<!-- !access-control-panel -->
</div> </div>

View File

@ -6,7 +6,9 @@ angular.module('portainer.azure').controller('AzureCreateContainerInstanceContro
'$state', '$state',
'AzureService', 'AzureService',
'Notifications', 'Notifications',
function ($q, $scope, $state, AzureService, Notifications) { 'Authentication',
'ResourceControlService',
function ($q, $scope, $state, AzureService, Notifications, Authentication, ResourceControlService) {
var allResourceGroups = []; var allResourceGroups = [];
var allProviders = []; var allProviders = [];
@ -42,12 +44,12 @@ angular.module('portainer.azure').controller('AzureCreateContainerInstanceContro
$scope.state.actionInProgress = true; $scope.state.actionInProgress = true;
AzureService.createContainerGroup(model, subscriptionId, resourceGroupName) AzureService.createContainerGroup(model, subscriptionId, resourceGroupName)
.then(function success() { .then(applyResourceControl)
.then(() => {
Notifications.success('Container successfully created', model.Name); Notifications.success('Container successfully created', model.Name);
$state.go('azure.containerinstances'); $state.go('azure.containerinstances');
}) })
.catch(function error(err) { .catch(function error(err) {
err = err.data ? err.data.error : err;
Notifications.error('Failure', err, 'Unable to create container'); Notifications.error('Failure', err, 'Unable to create container');
}) })
.finally(function final() { .finally(function final() {
@ -55,6 +57,14 @@ angular.module('portainer.azure').controller('AzureCreateContainerInstanceContro
}); });
}; };
function applyResourceControl(newResourceGroup) {
const userId = Authentication.getUserDetails().ID;
const resourceControl = newResourceGroup.Portainer.ResourceControl;
const accessControlData = $scope.model.AccessControlData;
return ResourceControlService.applyResourceControl(userId, accessControlData, resourceControl);
}
function validateForm(model) { function validateForm(model) {
if (!model.Ports || !model.Ports.length || model.Ports.every((port) => !port.host || !port.container)) { if (!model.Ports || !model.Ports.length || model.Ports.every((port) => !port.host || !port.container)) {
return 'At least one port binding is required'; return 'At least one port binding is required';
@ -73,7 +83,7 @@ angular.module('portainer.azure').controller('AzureCreateContainerInstanceContro
} }
function initView() { function initView() {
var model = new ContainerGroupDefaultModel(); $scope.model = new ContainerGroupDefaultModel();
AzureService.subscriptions() AzureService.subscriptions()
.then(function success(data) { .then(function success(data) {
@ -93,8 +103,6 @@ angular.module('portainer.azure').controller('AzureCreateContainerInstanceContro
var containerInstancesProviders = data.containerInstancesProviders; var containerInstancesProviders = data.containerInstancesProviders;
allProviders = containerInstancesProviders; allProviders = containerInstancesProviders;
$scope.model = model;
var selectedSubscription = $scope.state.selectedSubscription; var selectedSubscription = $scope.state.selectedSubscription;
updateResourceGroupsAndLocations(selectedSubscription, resourceGroups, containerInstancesProviders); updateResourceGroupsAndLocations(selectedSubscription, resourceGroups, containerInstancesProviders);
}) })

View File

@ -157,6 +157,9 @@
</div> </div>
</div> </div>
<!-- !memory-input --> <!-- !memory-input -->
<!-- access-control -->
<por-access-control-form form-data="model.AccessControlData"></por-access-control-form>
<!-- !access-control -->
<!-- actions --> <!-- actions -->
<div class="col-sm-12 form-section-title"> <div class="col-sm-12 form-section-title">
Actions Actions

View File

@ -7,6 +7,7 @@ export const ResourceControlTypeString = Object.freeze({
STACK: 'stack', STACK: 'stack',
VOLUME: 'volume', VOLUME: 'volume',
CUSTOM_TEMPLATE: 'custom-template', CUSTOM_TEMPLATE: 'custom-template',
CONTAINER_GROUP: 'container-group',
}); });
/** /**
@ -21,4 +22,5 @@ export const ResourceControlTypeInt = Object.freeze({
STACK: 6, STACK: 6,
CONFIG: 7, CONFIG: 7,
CUSTOM_TEMPLATE: 8, CUSTOM_TEMPLATE: 8,
CONTAINER_GROUP: 9,
}); });

View File

@ -26,6 +26,8 @@ angular.module('portainer.app').factory('Notifications', [
msg = e.data.message; msg = e.data.message;
} else if (e.data && e.data.content) { } else if (e.data && e.data.content) {
msg = e.data.content; msg = e.data.content;
} else if (e.data && e.data.error) {
msg = e.data.error;
} else if (e.message) { } else if (e.message) {
msg = e.message; msg = e.message;
} else if (e.err && e.err.data && e.err.data.length > 0 && e.err.data[0].message) { } else if (e.err && e.err.data && e.err.data.length > 0 && e.err.data[0].message) {