Skip to content

Commit

Permalink
feat: On window focus, redirect to login if the user has been logged …
Browse files Browse the repository at this point in the history
…out (#18773)

* /me api

* test it

* watch for window activation and check auth

* simplify

* more comment

* making ci happy

* mypy should ignore tests
  • Loading branch information
suddjian authored Feb 24, 2022
1 parent 94e245d commit da3bc48
Show file tree
Hide file tree
Showing 10 changed files with 168 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ describe('canUserEditDashboard', () => {
email: '[email protected]',
firstName: 'Test',
isActive: true,
isAnonymous: false,
lastName: 'User',
userId: 1,
username: 'owner',
Expand Down
30 changes: 28 additions & 2 deletions superset-frontend/src/preamble.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,24 @@ import { setConfig as setHotLoaderConfig } from 'react-hot-loader';
import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only';
import moment from 'moment';
// eslint-disable-next-line no-restricted-imports
import { configure, supersetTheme } from '@superset-ui/core';
import { configure, makeApi, supersetTheme } from '@superset-ui/core';
import { merge } from 'lodash';
import setupClient from './setup/setupClient';
import setupColors from './setup/setupColors';
import setupFormatters from './setup/setupFormatters';
import setupDashboardComponents from './setup/setupDasboardComponents';
import { User } from './types/bootstrapTypes';

if (process.env.WEBPACK_MODE === 'development') {
setHotLoaderConfig({ logLevel: 'debug', trackTailUpdates: false });
}

// eslint-disable-next-line import/no-mutable-exports
export let bootstrapData: any;
export let bootstrapData: {
user?: User | undefined;
common?: any;
config?: any;
} = {};
// Configure translation
if (typeof window !== 'undefined') {
const root = document.getElementById('app');
Expand Down Expand Up @@ -67,3 +72,24 @@ export const theme = merge(
supersetTheme,
bootstrapData?.common?.theme_overrides ?? {},
);

const getMe = makeApi<void, User>({
method: 'GET',
endpoint: '/api/v1/me/',
});

/**
* When you re-open the window, we check if you are still logged in.
* If your session expired or you signed out, we'll redirect to login.
* If you aren't logged in in the first place (!isActive), then we shouldn't do this.
*/
if (bootstrapData.user?.isActive) {
document.addEventListener('visibilitychange', () => {
// we only care about the tab becoming visible, not vice versa
if (document.visibilityState !== 'visible') return;

getMe().catch(() => {
// ignore error, SupersetClient will redirect to login on a 401
});
});
}
1 change: 1 addition & 0 deletions superset-frontend/src/profile/components/fixtures.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export const user: UserWithPermissionsAndRoles = {
userId: 5,
email: '[email protected]',
isActive: true,
isAnonymous: false,
permissions: {
datasource_access: ['table1', 'table2'],
database_access: ['db1', 'db2', 'db3'],
Expand Down
1 change: 1 addition & 0 deletions superset-frontend/src/types/bootstrapTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export type User = {
email: string;
firstName: string;
isActive: boolean;
isAnonymous: boolean;
lastName: string;
userId: number;
username: string;
Expand Down
2 changes: 2 additions & 0 deletions superset/initialization/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
)
from superset.security import SupersetSecurityManager
from superset.typing import FlaskResponse
from superset.users.api import CurrentUserRestApi
from superset.utils.core import pessimistic_connection_handling
from superset.utils.log import DBEventLogger, get_event_logger_from_cfg_value

Expand Down Expand Up @@ -205,6 +206,7 @@ def init_views(self) -> None:
appbuilder.add_api(ChartRestApi)
appbuilder.add_api(ChartDataRestApi)
appbuilder.add_api(CssTemplateRestApi)
appbuilder.add_api(CurrentUserRestApi)
appbuilder.add_api(DashboardFilterStateRestApi)
appbuilder.add_api(DashboardRestApi)
appbuilder.add_api(DatabaseRestApi)
Expand Down
56 changes: 56 additions & 0 deletions superset/users/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you 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.
from flask import g, Response
from flask_appbuilder.api import BaseApi, expose, safe

from .schemas import UserResponseSchema

user_response_schema = UserResponseSchema()


class CurrentUserRestApi(BaseApi):
""" An api to get information about the current user """

resource_name = "me"
openapi_spec_tag = "Current User"
openapi_spec_component_schemas = (UserResponseSchema,)

@expose("/", methods=["GET"])
@safe
def me(self) -> Response:
"""Get the user object corresponding to the agent making the request
---
get:
description: >-
Returns the user object corresponding to the agent making the request,
or returns a 401 error if the user is unauthenticated.
responses:
200:
description: The current user
content:
application/json:
schema:
type: object
properties:
result:
$ref: '#/components/schemas/UserResponseSchema'
401:
$ref: '#/components/responses/401'
"""
if g.user is None or g.user.is_anonymous:
return self.response_401()
return self.response(200, result=user_response_schema.dump(g.user))
28 changes: 28 additions & 0 deletions superset/users/schemas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you 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.
from marshmallow import Schema
from marshmallow.fields import Boolean, Integer, String


class UserResponseSchema(Schema):
id = Integer()
username = String()
email = String()
first_name = String()
last_name = String()
is_active = Boolean()
is_anonymous = Boolean()
1 change: 1 addition & 0 deletions superset/views/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ def bootstrap_user_data(user: User, include_perms: bool = False) -> Dict[str, An
"lastName": user.last_name,
"userId": user.id,
"isActive": user.is_active,
"isAnonymous": user.is_anonymous,
"createdOn": user.created_on.isoformat(),
"email": user.email,
}
Expand Down
1 change: 1 addition & 0 deletions tests/integration_tests/security_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -906,6 +906,7 @@ def test_views_are_secured(self):
["LocaleView", "index"],
["AuthDBView", "login"],
["AuthDBView", "logout"],
["CurrentUserRestApi", "me"],
["Dashboard", "embedded"],
["R", "index"],
["Superset", "log"],
Expand Down
49 changes: 49 additions & 0 deletions tests/integration_tests/users/api_tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you 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.
# type: ignore
"""Unit tests for Superset"""
import json
from unittest.mock import patch

from superset import security_manager
from tests.integration_tests.base_tests import SupersetTestCase

meUri = "/api/v1/me/"


class TestCurrentUserApi(SupersetTestCase):
def test_get_me_logged_in(self):
self.login(username="admin")

rv = self.client.get(meUri)

self.assertEqual(200, rv.status_code)
response = json.loads(rv.data.decode("utf-8"))
self.assertEqual("admin", response["result"]["username"])
self.assertEqual(True, response["result"]["is_active"])
self.assertEqual(False, response["result"]["is_anonymous"])

def test_get_me_unauthorized(self):
self.logout()
rv = self.client.get(meUri)
self.assertEqual(401, rv.status_code)

@patch("superset.security.manager.g")
def test_get_me_anonymous(self, mock_g):
mock_g.user = security_manager.get_anonymous_user
rv = self.client.get(meUri)
self.assertEqual(401, rv.status_code)

0 comments on commit da3bc48

Please sign in to comment.