Social account authentication tests (#5444)

Depends on #5349
Related #5432
Added tests for social account authentication functionality: cypress
test with dummy auth server
main
Maria Khrustaleva 3 years ago committed by GitHub
parent 71a0aaf2bb
commit b00bc653ff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -327,11 +327,13 @@ jobs:
- name: Run CVAT instance - name: Run CVAT instance
run: | run: |
docker compose \ docker compose \
--env-file="tests/python/social_auth/.env" \
-f docker-compose.yml \ -f docker-compose.yml \
-f docker-compose.dev.yml \ -f docker-compose.dev.yml \
-f components/serverless/docker-compose.serverless.yml \ -f components/serverless/docker-compose.serverless.yml \
-f tests/docker-compose.minio.yml \ -f tests/docker-compose.minio.yml \
-f tests/docker-compose.file_share.yml up -d -f tests/docker-compose.file_share.yml \
-f tests/python/social_auth/docker-compose.yml up -d
- name: Waiting for server - name: Waiting for server
env: env:

@ -294,12 +294,13 @@ jobs:
- name: Run CVAT instance - name: Run CVAT instance
run: | run: |
docker compose \ docker compose \
--env-file="tests/python/social_auth/.env" \
-f docker-compose.yml \ -f docker-compose.yml \
-f docker-compose.dev.yml \ -f docker-compose.dev.yml \
-f components/serverless/docker-compose.serverless.yml \ -f components/serverless/docker-compose.serverless.yml \
-f tests/docker-compose.minio.yml \ -f tests/docker-compose.minio.yml \
-f tests/docker-compose.file_share.yml up -d -f tests/docker-compose.file_share.yml \
-f tests/python/social_auth/docker-compose.yml up -d
- name: Waiting for server - name: Waiting for server
env: env:
API_ABOUT_PAGE: "localhost:8080/api/server/about" API_ABOUT_PAGE: "localhost:8080/api/server/about"

@ -319,11 +319,13 @@ jobs:
- name: Run CVAT instance - name: Run CVAT instance
run: | run: |
docker compose \ docker compose \
--env-file="tests/python/social_auth/.env" \
-f docker-compose.yml \ -f docker-compose.yml \
-f docker-compose.dev.yml \ -f docker-compose.dev.yml \
-f tests/docker-compose.file_share.yml \ -f tests/docker-compose.file_share.yml \
-f tests/docker-compose.minio.yml \ -f tests/docker-compose.minio.yml \
-f components/serverless/docker-compose.serverless.yml up -d -f components/serverless/docker-compose.serverless.yml \
-f tests/python/social_auth/docker-compose.yml up -d
- name: Waiting for server - name: Waiting for server
id: wait-server id: wait-server

@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- \[SDK\] A PyTorch adapter setting to disable cache updates - \[SDK\] A PyTorch adapter setting to disable cache updates
(<https://github.com/opencv/cvat/pull/5549>) (<https://github.com/opencv/cvat/pull/5549>)
- YOLO v7 serverless feature added using ONNX backend (<https://github.com/opencv/cvat/pull/5552>) - YOLO v7 serverless feature added using ONNX backend (<https://github.com/opencv/cvat/pull/5552>)
- Cypress test for social account authentication (<https://github.com/opencv/cvat/pull/5444>)
- Dummy github and google authentication servers (<https://github.com/opencv/cvat/pull/5444>)
### Changed ### Changed
- The Docker Compose files now use the Compose Specification version - The Docker Compose files now use the Compose Specification version

@ -1,6 +1,6 @@
{ {
"name": "cvat-core", "name": "cvat-core",
"version": "7.4.1", "version": "7.5.0",
"description": "Part of Computer Vision Tool which presents an interface for client-side integration", "description": "Part of Computer Vision Tool which presents an interface for client-side integration",
"main": "src/api.ts", "main": "src/api.ts",
"scripts": { "scripts": {

@ -3,6 +3,7 @@
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import { SocialAuthMethod, SocialAuthMethodsRawType } from './auth-methods';
import config from './config'; import config from './config';
import PluginRegistry from './plugins'; import PluginRegistry from './plugins';
@ -85,9 +86,9 @@ export default function implementAPI(cvat) {
await serverProxy.server.logout(); await serverProxy.server.logout();
}; };
cvat.server.advancedAuthentication.implementation = async () => { cvat.server.socialAuthentication.implementation = async () => {
const result = await serverProxy.server.advancedAuthentication(); const result: SocialAuthMethodsRawType = await serverProxy.server.socialAuthentication();
return result; return Object.entries(result).map(([provider, value]) => new SocialAuthMethod({ ...value, provider }));
}; };
cvat.server.changePassword.implementation = async (oldPassword, newPassword1, newPassword2) => { cvat.server.changePassword.implementation = async (oldPassword, newPassword1, newPassword2) => {

@ -77,8 +77,8 @@ function build() {
const result = await PluginRegistry.apiWrapper(cvat.server.logout); const result = await PluginRegistry.apiWrapper(cvat.server.logout);
return result; return result;
}, },
async advancedAuthentication() { async socialAuthentication() {
const result = await PluginRegistry.apiWrapper(cvat.server.advancedAuthentication); const result = await PluginRegistry.apiWrapper(cvat.server.socialAuthentication);
return result; return result;
}, },
async changePassword(oldPassword, newPassword1, newPassword2) { async changePassword(oldPassword, newPassword1, newPassword2) {

@ -0,0 +1,57 @@
// Copyright (C) 2022 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
interface SocialAuthMethodCamelCase {
provider: string;
publicName: string;
isEnabled: boolean;
icon: string;
}
interface SocialAuthMethodSnakeCase {
public_name: string;
is_enabled: boolean;
icon: string;
provider?: string;
}
export class SocialAuthMethod {
public provider: string;
public publicName: string;
public isEnabled: boolean;
public icon: string;
constructor(initialData: SocialAuthMethodSnakeCase) {
const data: SocialAuthMethodCamelCase = {
provider: initialData.provider,
publicName: initialData.public_name,
isEnabled: initialData.is_enabled,
icon: initialData.icon,
};
Object.defineProperties(
this,
Object.freeze({
provider: {
get: () => data.provider,
},
publicName: {
get: () => data.publicName,
},
isEnabled: {
get: () => data.isEnabled,
},
icon: {
get: () => data.icon,
},
}),
);
}
}
export type SocialAuthMethodsRawType = {
[index: string]: SocialAuthMethodSnakeCase;
};
export type SocialAuthMethods = SocialAuthMethod[];

@ -2250,10 +2250,10 @@ async function receiveWebhookEvents(type: WebhookSourceType): Promise<string[]>
} }
} }
async function advancedAuthentication(): Promise<any> { async function socialAuthentication(): Promise<any> {
const { backendAPI } = config; const { backendAPI } = config;
try { try {
const response = await Axios.get(`${backendAPI}/server/advanced-auth`, { const response = await Axios.get(`${backendAPI}/auth/social/methods`, {
proxy: config.proxy, proxy: config.proxy,
}); });
return response.data; return response.data;
@ -2270,7 +2270,7 @@ export default Object.freeze({
exception, exception,
login, login,
logout, logout,
advancedAuthentication, socialAuthentication,
changePassword, changePassword,
requestPasswordReset, requestPasswordReset,
resetPassword, resetPassword,

@ -7,7 +7,7 @@ import { ActionUnion, createAction, ThunkAction } from 'utils/redux';
import { UserConfirmation } from 'components/register-page/register-form'; import { UserConfirmation } from 'components/register-page/register-form';
import { getCore } from 'cvat-core-wrapper'; import { getCore } from 'cvat-core-wrapper';
import isReachable from 'utils/url-checker'; import isReachable from 'utils/url-checker';
import { AdvancedAuthMethodsList } from '../reducers'; import { SocialAuthMethods } from '../cvat-core-wrapper';
const cvat = getCore(); const cvat = getCore();
@ -36,9 +36,9 @@ export enum AuthActionTypes {
LOAD_AUTH_ACTIONS = 'LOAD_AUTH_ACTIONS', LOAD_AUTH_ACTIONS = 'LOAD_AUTH_ACTIONS',
LOAD_AUTH_ACTIONS_SUCCESS = 'LOAD_AUTH_ACTIONS_SUCCESS', LOAD_AUTH_ACTIONS_SUCCESS = 'LOAD_AUTH_ACTIONS_SUCCESS',
LOAD_AUTH_ACTIONS_FAILED = 'LOAD_AUTH_ACTIONS_FAILED', LOAD_AUTH_ACTIONS_FAILED = 'LOAD_AUTH_ACTIONS_FAILED',
LOAD_ADVANCED_AUTHENTICATION = 'LOAD_ADVANCED_AUTHENTICATION', LOAD_SOCIAL_AUTHENTICATION = 'LOAD_SOCIAL_AUTHENTICATION',
LOAD_ADVANCED_AUTHENTICATION_SUCCESS = 'LOAD_ADVANCED_AUTHENTICATION_SUCCESS', LOAD_SOCIAL_AUTHENTICATION_SUCCESS = 'LOAD_SOCIAL_AUTHENTICATION_SUCCESS',
LOAD_ADVANCED_AUTHENTICATION_FAILED = 'LOAD_ADVANCED_AUTHENTICATION_FAILED', LOAD_SOCIAL_AUTHENTICATION_FAILED = 'LOAD_SOCIAL_AUTHENTICATION_FAILED',
} }
export const authActions = { export const authActions = {
@ -75,12 +75,12 @@ export const authActions = {
}) })
), ),
loadServerAuthActionsFailed: (error: any) => createAction(AuthActionTypes.LOAD_AUTH_ACTIONS_FAILED, { error }), loadServerAuthActionsFailed: (error: any) => createAction(AuthActionTypes.LOAD_AUTH_ACTIONS_FAILED, { error }),
loadAdvancedAuth: () => createAction(AuthActionTypes.LOAD_ADVANCED_AUTHENTICATION), loadSocialAuth: () => createAction(AuthActionTypes.LOAD_SOCIAL_AUTHENTICATION),
loadAdvancedAuthSuccess: (list: AdvancedAuthMethodsList) => ( loadSocialAuthSuccess: (methods: SocialAuthMethods) => (
createAction(AuthActionTypes.LOAD_ADVANCED_AUTHENTICATION_SUCCESS, { list }) createAction(AuthActionTypes.LOAD_SOCIAL_AUTHENTICATION_SUCCESS, { methods })
), ),
loadAdvancedAuthFailed: (error: any) => ( loadSocialAuthFailed: (error: any) => (
createAction(AuthActionTypes.LOAD_ADVANCED_AUTHENTICATION_FAILED, { error }) createAction(AuthActionTypes.LOAD_SOCIAL_AUTHENTICATION_FAILED, { error })
), ),
}; };
@ -210,12 +210,12 @@ export const loadAuthActionsAsync = (): ThunkAction => async (dispatch) => {
} }
}; };
export const loadAdvancedAuthAsync = (): ThunkAction => async (dispatch): Promise<void> => { export const loadSocialAuthAsync = (): ThunkAction => async (dispatch): Promise<void> => {
dispatch(authActions.loadAdvancedAuth()); dispatch(authActions.loadSocialAuth());
try { try {
const list: AdvancedAuthMethodsList = await cvat.server.advancedAuthentication(); const methods: SocialAuthMethods = await cvat.server.socialAuthentication();
dispatch(authActions.loadAdvancedAuthSuccess(list)); dispatch(authActions.loadSocialAuthSuccess(methods));
} catch (error) { } catch (error) {
dispatch(authActions.loadAdvancedAuthFailed(error)); dispatch(authActions.loadSocialAuthFailed(error));
} }
}; };

@ -10,9 +10,8 @@ import { Row, Col } from 'antd/lib/grid';
import SigningLayout, { formSizes } from 'components/signing-common/signing-layout'; import SigningLayout, { formSizes } from 'components/signing-common/signing-layout';
import SocialAccountLink from 'components/signing-common/social-account-link'; import SocialAccountLink from 'components/signing-common/social-account-link';
import { SocialGithubLogo, SocialGoogleLogo } from 'icons';
import LoginForm, { LoginData } from './login-form'; import LoginForm, { LoginData } from './login-form';
import { getCore } from '../../cvat-core-wrapper'; import { getCore, SocialAuthMethods, SocialAuthMethod } from '../../cvat-core-wrapper';
const cvat = getCore(); const cvat = getCore();
@ -20,18 +19,31 @@ interface LoginPageComponentProps {
fetching: boolean; fetching: boolean;
renderResetPassword: boolean; renderResetPassword: boolean;
hasEmailVerificationBeenSent: boolean; hasEmailVerificationBeenSent: boolean;
googleAuthentication: boolean; socialAuthMethods: SocialAuthMethods;
githubAuthentication: boolean;
onLogin: (credential: string, password: string) => void; onLogin: (credential: string, password: string) => void;
loadAdvancedAuthenticationMethods: () => void; loadSocialAuthenticationMethods: () => void;
} }
const renderSocialAuthMethods = (methods: SocialAuthMethods): JSX.Element[] => {
const { backendAPI } = cvat.config;
return methods.map((item: SocialAuthMethod) => ((item.isEnabled) ? (
<SocialAccountLink
key={item.provider}
icon={item.icon}
href={`${backendAPI}/auth/${item.provider}/login`}
className={`cvat-social-authentication-${item.provider}`}
>
{`Continue with ${item.publicName}`}
</SocialAccountLink>
) : <></>));
};
function LoginPageComponent(props: LoginPageComponentProps & RouteComponentProps): JSX.Element { function LoginPageComponent(props: LoginPageComponentProps & RouteComponentProps): JSX.Element {
const history = useHistory(); const history = useHistory();
const { backendAPI } = cvat.config;
const { const {
fetching, renderResetPassword, hasEmailVerificationBeenSent, fetching, renderResetPassword, hasEmailVerificationBeenSent,
googleAuthentication, githubAuthentication, onLogin, loadAdvancedAuthenticationMethods, socialAuthMethods, onLogin, loadSocialAuthenticationMethods,
} = props; } = props;
if (hasEmailVerificationBeenSent) { if (hasEmailVerificationBeenSent) {
@ -39,7 +51,7 @@ function LoginPageComponent(props: LoginPageComponentProps & RouteComponentProps
} }
useEffect(() => { useEffect(() => {
loadAdvancedAuthenticationMethods(); loadSocialAuthenticationMethods();
}, []); }, []);
return ( return (
@ -50,26 +62,9 @@ function LoginPageComponent(props: LoginPageComponentProps & RouteComponentProps
<LoginForm <LoginForm
fetching={fetching} fetching={fetching}
renderResetPassword={renderResetPassword} renderResetPassword={renderResetPassword}
socialAuthentication={(googleAuthentication || githubAuthentication) ? ( socialAuthentication={(socialAuthMethods) ? (
<div className='cvat-social-authentication'> <div className='cvat-social-authentication'>
{githubAuthentication && ( {renderSocialAuthMethods(socialAuthMethods)}
<SocialAccountLink
icon={SocialGithubLogo}
href={`${backendAPI}/auth/github/login`}
className='cvat-social-authentication-github'
>
Continue with GitHub
</SocialAccountLink>
)}
{googleAuthentication && (
<SocialAccountLink
icon={SocialGoogleLogo}
href={`${backendAPI}/auth/google/login`}
className='cvat-social-authentication-google'
>
Continue with Google
</SocialAccountLink>
)}
</div> </div>
) : null} ) : null}
onSubmit={(loginData: LoginData): void => { onSubmit={(loginData: LoginData): void => {

@ -3,7 +3,7 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { useLocation, useHistory } from 'react-router'; import { Redirect, useLocation, useHistory } from 'react-router';
import notification from 'antd/lib/notification'; import notification from 'antd/lib/notification';
import Spin from 'antd/lib/spin'; import Spin from 'antd/lib/spin';
@ -29,6 +29,7 @@ export default function LoginWithSocialAppComponent(): JSX.Element {
.catch((exception: Error) => { .catch((exception: Error) => {
if (exception.message.includes('Unverified email')) { if (exception.message.includes('Unverified email')) {
history.push('/auth/email-verification-sent'); history.push('/auth/email-verification-sent');
return Promise.resolve();
} }
history.push('/auth/login'); history.push('/auth/login');
notification.error({ notification.error({
@ -40,6 +41,10 @@ export default function LoginWithSocialAppComponent(): JSX.Element {
} }
}, []); }, []);
if (localStorage.getItem('token')) {
return <Redirect to={search.get('next') || '/tasks'} />;
}
return ( return (
<div className='cvat-login-page cvat-spinner-container'> <div className='cvat-login-page cvat-spinner-container'>
<Spin size='large' className='cvat-spinner' /> <Spin size='large' className='cvat-spinner' />

@ -5,20 +5,27 @@ import './styles.scss';
import React from 'react'; import React from 'react';
import { Col, Row } from 'antd/lib/grid'; import { Col, Row } from 'antd/lib/grid';
import Button from 'antd/lib/button/button'; import Button from 'antd/lib/button/button';
import Icon from '@ant-design/icons';
import { CustomIconComponentProps } from '@ant-design/icons/lib/components/Icon';
interface SocialAccountLinkProps { interface SocialAccountLinkProps {
children: string; children: string;
className?: string; className?: string;
href: string; href: string;
icon: React.ForwardRefExoticComponent<CustomIconComponentProps>; icon: string;
} }
function SocialAccountLink(props: SocialAccountLinkProps): JSX.Element { function SocialAccountLink(props: SocialAccountLinkProps): JSX.Element {
const svgWrapperRef = React.useRef();
const { const {
children, className, href, icon, children, className, href, icon,
} = props; } = props;
React.useEffect(() => {
if (icon) {
// eslint-disable-next-line no-unsanitized/property
svgWrapperRef.current.innerHTML = icon;
}
}, [icon, svgWrapperRef.current]);
return ( return (
<Row> <Row>
<Col flex='auto'> <Col flex='auto'>
@ -28,7 +35,10 @@ function SocialAccountLink(props: SocialAccountLinkProps): JSX.Element {
> >
<Row align='middle' style={{ width: '100%' }}> <Row align='middle' style={{ width: '100%' }}>
<Col> <Col>
<Icon component={icon} /> <div
ref={svgWrapperRef as any}
className='cvat-social-authentication-icon'
/>
</Col> </Col>
<Col flex='auto'> <Col flex='auto'>
{children} {children}

@ -5,19 +5,19 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import LoginPageComponent from 'components/login-page/login-page'; import LoginPageComponent from 'components/login-page/login-page';
import { CombinedState } from 'reducers'; import { CombinedState } from 'reducers';
import { loginAsync, loadAdvancedAuthAsync } from 'actions/auth-actions'; import { loginAsync, loadSocialAuthAsync } from 'actions/auth-actions';
import { SocialAuthMethods } from 'cvat-core-wrapper';
interface StateToProps { interface StateToProps {
fetching: boolean; fetching: boolean;
renderResetPassword: boolean; renderResetPassword: boolean;
hasEmailVerificationBeenSent: boolean; hasEmailVerificationBeenSent: boolean;
googleAuthentication: boolean; socialAuthMethods: SocialAuthMethods;
githubAuthentication: boolean;
} }
interface DispatchToProps { interface DispatchToProps {
onLogin: typeof loginAsync; onLogin: typeof loginAsync;
loadAdvancedAuthenticationMethods: typeof loadAdvancedAuthAsync; loadSocialAuthenticationMethods: typeof loadSocialAuthAsync;
} }
function mapStateToProps(state: CombinedState): StateToProps { function mapStateToProps(state: CombinedState): StateToProps {
@ -25,14 +25,13 @@ function mapStateToProps(state: CombinedState): StateToProps {
fetching: state.auth.fetching, fetching: state.auth.fetching,
renderResetPassword: state.auth.allowResetPassword, renderResetPassword: state.auth.allowResetPassword,
hasEmailVerificationBeenSent: state.auth.hasEmailVerificationBeenSent, hasEmailVerificationBeenSent: state.auth.hasEmailVerificationBeenSent,
googleAuthentication: state.auth.advancedAuthList.GOOGLE_ACCOUNT_AUTHENTICATION, socialAuthMethods: state.auth.socialAuthMethods,
githubAuthentication: state.auth.advancedAuthList.GITHUB_ACCOUNT_AUTHENTICATION,
}; };
} }
const mapDispatchToProps: DispatchToProps = { const mapDispatchToProps: DispatchToProps = {
onLogin: loginAsync, onLogin: loginAsync,
loadAdvancedAuthenticationMethods: loadAdvancedAuthAsync, loadSocialAuthenticationMethods: loadSocialAuthAsync,
}; };
export default connect(mapStateToProps, mapDispatchToProps)(LoginPageComponent); export default connect(mapStateToProps, mapDispatchToProps)(LoginPageComponent);

@ -10,6 +10,7 @@ import {
} from 'cvat-core/src/labels'; } from 'cvat-core/src/labels';
import { ShapeType, LabelType } from 'cvat-core/src/enums'; import { ShapeType, LabelType } from 'cvat-core/src/enums';
import { Storage, StorageData } from 'cvat-core/src/storage'; import { Storage, StorageData } from 'cvat-core/src/storage';
import { SocialAuthMethods, SocialAuthMethod } from 'cvat-core/src/auth-methods';
const cvat: any = _cvat; const cvat: any = _cvat;
@ -31,10 +32,12 @@ export {
LabelType, LabelType,
Storage, Storage,
Webhook, Webhook,
SocialAuthMethod,
}; };
export type { export type {
RawAttribute, RawAttribute,
RawLabel, RawLabel,
StorageData, StorageData,
SocialAuthMethods,
}; };

@ -62,8 +62,6 @@ import SVGMultiPlusIcon from './assets/multi-plus-icon.svg';
import SVGBackArrowIcon from './assets/back-arrow-icon.svg'; import SVGBackArrowIcon from './assets/back-arrow-icon.svg';
import SVGClearIcon from './assets/clear-icon.svg'; import SVGClearIcon from './assets/clear-icon.svg';
import SVGShowPasswordIcon from './assets/show-password.svg'; import SVGShowPasswordIcon from './assets/show-password.svg';
import SVGSocialGithubLogo from './assets/social-github-logo.svg';
import SVGSocialGoogleLogo from './assets/social-google-logo.svg';
import SVGPlusIcon from './assets/plus-icon.svg'; import SVGPlusIcon from './assets/plus-icon.svg';
import SVGCheckIcon from './assets/check-icon.svg'; import SVGCheckIcon from './assets/check-icon.svg';
@ -124,7 +122,5 @@ export const MutliPlusIcon = React.memo((): JSX.Element => <SVGMultiPlusIcon />)
export const BackArrowIcon = React.memo((): JSX.Element => <SVGBackArrowIcon />); export const BackArrowIcon = React.memo((): JSX.Element => <SVGBackArrowIcon />);
export const ClearIcon = React.memo((): JSX.Element => <SVGClearIcon />); export const ClearIcon = React.memo((): JSX.Element => <SVGClearIcon />);
export const ShowPasswordIcon = React.memo((): JSX.Element => <SVGShowPasswordIcon />); export const ShowPasswordIcon = React.memo((): JSX.Element => <SVGShowPasswordIcon />);
export const SocialGithubLogo = React.memo((): JSX.Element => <SVGSocialGithubLogo />);
export const SocialGoogleLogo = React.memo((): JSX.Element => <SVGSocialGoogleLogo />);
export const PlusIcon = React.memo((): JSX.Element => <SVGPlusIcon />); export const PlusIcon = React.memo((): JSX.Element => <SVGPlusIcon />);
export const CheckIcon = React.memo((): JSX.Element => <SVGCheckIcon />); export const CheckIcon = React.memo((): JSX.Element => <SVGCheckIcon />);

@ -16,12 +16,9 @@ const defaultState: AuthState = {
showChangePasswordDialog: false, showChangePasswordDialog: false,
allowResetPassword: false, allowResetPassword: false,
hasEmailVerificationBeenSent: false, hasEmailVerificationBeenSent: false,
advancedAuthFetching: false, socialAuthFetching: false,
advancedAuthInitialized: false, socialAuthInitialized: false,
advancedAuthList: { socialAuthMethods: [],
GOOGLE_ACCOUNT_AUTHENTICATION: false,
GITHUB_ACCOUNT_AUTHENTICATION: false,
},
}; };
export default function (state = defaultState, action: AuthActions | BoundariesActions): AuthState { export default function (state = defaultState, action: AuthActions | BoundariesActions): AuthState {
@ -160,27 +157,27 @@ export default function (state = defaultState, action: AuthActions | BoundariesA
allowChangePassword: false, allowChangePassword: false,
allowResetPassword: false, allowResetPassword: false,
}; };
case AuthActionTypes.LOAD_ADVANCED_AUTHENTICATION: { case AuthActionTypes.LOAD_SOCIAL_AUTHENTICATION: {
return { return {
...state, ...state,
advancedAuthFetching: true, socialAuthFetching: true,
advancedAuthInitialized: false, socialAuthInitialized: false,
}; };
} }
case AuthActionTypes.LOAD_ADVANCED_AUTHENTICATION_SUCCESS: { case AuthActionTypes.LOAD_SOCIAL_AUTHENTICATION_SUCCESS: {
const { list } = action.payload; const { methods } = action.payload;
return { return {
...state, ...state,
advancedAuthFetching: false, socialAuthFetching: false,
advancedAuthInitialized: true, socialAuthInitialized: true,
advancedAuthList: list, socialAuthMethods: methods,
}; };
} }
case AuthActionTypes.LOAD_ADVANCED_AUTHENTICATION_FAILED: { case AuthActionTypes.LOAD_SOCIAL_AUTHENTICATION_FAILED: {
return { return {
...state, ...state,
advancedAuthFetching: false, socialAuthFetching: false,
advancedAuthInitialized: true, socialAuthInitialized: true,
}; };
} }
case BoundariesActionTypes.RESET_AFTER_ERROR: { case BoundariesActionTypes.RESET_AFTER_ERROR: {

@ -5,7 +5,7 @@
import { Canvas3d } from 'cvat-canvas3d/src/typescript/canvas3d'; import { Canvas3d } from 'cvat-canvas3d/src/typescript/canvas3d';
import { Canvas, RectDrawingMethod, CuboidDrawingMethod } from 'cvat-canvas-wrapper'; import { Canvas, RectDrawingMethod, CuboidDrawingMethod } from 'cvat-canvas-wrapper';
import { Webhook } from 'cvat-core-wrapper'; import { Webhook, SocialAuthMethods } from 'cvat-core-wrapper';
import { IntelligentScissors } from 'utils/opencv-wrapper/intelligent-scissors'; import { IntelligentScissors } from 'utils/opencv-wrapper/intelligent-scissors';
import { KeyMap } from 'utils/mousetrap-react'; import { KeyMap } from 'utils/mousetrap-react';
import { OpenCVTracker } from 'utils/opencv-wrapper/opencv-interfaces'; import { OpenCVTracker } from 'utils/opencv-wrapper/opencv-interfaces';
@ -14,15 +14,6 @@ export type StringObject = {
[index: string]: string; [index: string]: string;
}; };
enum AdvancedAuthMethods {
GOOGLE_ACCOUNT_AUTHENTICATION = 'GOOGLE_ACCOUNT_AUTHENTICATION',
GITHUB_ACCOUNT_AUTHENTICATION = 'GITHUB_ACCOUNT_AUTHENTICATION',
}
export type AdvancedAuthMethodsList = {
[name in AdvancedAuthMethods]: boolean;
};
export interface AuthState { export interface AuthState {
initialized: boolean; initialized: boolean;
fetching: boolean; fetching: boolean;
@ -33,9 +24,9 @@ export interface AuthState {
allowChangePassword: boolean; allowChangePassword: boolean;
allowResetPassword: boolean; allowResetPassword: boolean;
hasEmailVerificationBeenSent: boolean; hasEmailVerificationBeenSent: boolean;
advancedAuthFetching: boolean; socialAuthFetching: boolean;
advancedAuthInitialized: boolean; socialAuthInitialized: boolean;
advancedAuthList: AdvancedAuthMethodsList; socialAuthMethods: SocialAuthMethods;
} }
export interface ProjectsQuery { export interface ProjectsQuery {

@ -27,7 +27,7 @@ from django.utils import timezone
from drf_spectacular.types import OpenApiTypes from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import ( from drf_spectacular.utils import (
OpenApiParameter, OpenApiResponse, PolymorphicProxySerializer, OpenApiParameter, OpenApiResponse, PolymorphicProxySerializer,
extend_schema_view, extend_schema, inline_serializer extend_schema_view, extend_schema
) )
from drf_spectacular.plumbing import build_array_type, build_basic_type from drf_spectacular.plumbing import build_array_type, build_basic_type
@ -234,39 +234,6 @@ class ServerViewSet(viewsets.ViewSet):
} }
return Response(response) return Response(response)
@staticmethod
@extend_schema(
summary='Method provides a list with advanced integrated authentication methods (e.g. social accounts)',
responses={
'200': OpenApiResponse(response=inline_serializer(
name='AdvancedAuthentication',
fields={
'GOOGLE_ACCOUNT_AUTHENTICATION': serializers.BooleanField(),
'GITHUB_ACCOUNT_AUTHENTICATION': serializers.BooleanField(),
}
)),
}
)
@action(detail=False, methods=['GET'], url_path='advanced-auth', permission_classes=[])
def advanced_authentication(request):
use_social_auth = settings.USE_ALLAUTH_SOCIAL_ACCOUNTS
integrated_auth_providers = settings.SOCIALACCOUNT_PROVIDERS.keys() if use_social_auth else []
google_auth_is_enabled = bool(
'google' in integrated_auth_providers
and settings.SOCIAL_AUTH_GOOGLE_CLIENT_ID
and settings.SOCIAL_AUTH_GOOGLE_CLIENT_SECRET
)
github_auth_is_enabled = bool(
'github' in integrated_auth_providers
and settings.SOCIAL_AUTH_GITHUB_CLIENT_ID
and settings.SOCIAL_AUTH_GITHUB_CLIENT_SECRET
)
response = {
'GOOGLE_ACCOUNT_AUTHENTICATION': google_auth_is_enabled,
'GITHUB_ACCOUNT_AUTHENTICATION': github_auth_is_enabled,
}
return Response(response)
@extend_schema(tags=['projects']) @extend_schema(tags=['projects'])
@extend_schema_view( @extend_schema_view(
list=extend_schema( list=extend_schema(

@ -40,9 +40,11 @@ class SocialAccountAdapterEx(DefaultSocialAccountAdapter):
return return
class GitHubAdapter(GitHubOAuth2Adapter): class GitHubAdapter(GitHubOAuth2Adapter):
def get_callback_url(self, request, app): def get_callback_url(self, request, app):
return settings.GITHUB_CALLBACK_URL return settings.GITHUB_CALLBACK_URL
class GoogleAdapter(GoogleOAuth2Adapter): class GoogleAdapter(GoogleOAuth2Adapter):
def get_callback_url(self, request, app): def get_callback_url(self, request, app):
return settings.GOOGLE_CALLBACK_URL return settings.GOOGLE_CALLBACK_URL

@ -368,7 +368,6 @@ class ServerPermission(OpenPolicyAgentPermission):
'exception': Scopes.SEND_EXCEPTION, 'exception': Scopes.SEND_EXCEPTION,
'logs': Scopes.SEND_LOGS, 'logs': Scopes.SEND_LOGS,
'share': Scopes.LIST_CONTENT, 'share': Scopes.LIST_CONTENT,
'advanced_authentication': Scopes.VIEW,
}.get(view.action, None)] }.get(view.action, None)]
def get_resource(self): def get_resource(self):

@ -1,5 +1,5 @@
Scope,Resource,Context,Ownership,Limit,Method,URL,Privilege,Membership Scope,Resource,Context,Ownership,Limit,Method,URL,Privilege,Membership
view,N/A,N/A,N/A,,GET,"/server/about, /server/annotation/formats, /server/plugins, /server/advanced-auth",None,N/A view,N/A,N/A,N/A,,GET,"/server/about, /server/annotation/formats, /server/plugins",None,N/A
send:exception,N/A,N/A,N/A,,POST,/server/exception,None,N/A send:exception,N/A,N/A,N/A,,POST,/server/exception,None,N/A
send:logs,N/A,N/A,N/A,,POST,/server/logs,None,N/A send:logs,N/A,N/A,N/A,,POST,/server/logs,None,N/A
list:content,N/A,N/A,N/A,,GET,/server/share,Worker,N/A list:content,N/A,N/A,N/A,,GET,/server/share,Worker,N/A
1 Scope Resource Context Ownership Limit Method URL Privilege Membership
2 view N/A N/A N/A GET /server/about, /server/annotation/formats, /server/plugins, /server/advanced-auth /server/about, /server/annotation/formats, /server/plugins None N/A
3 send:exception N/A N/A N/A POST /server/exception None N/A
4 send:logs N/A N/A N/A POST /server/logs None N/A
5 list:content N/A N/A N/A GET /server/share Worker N/A

@ -98,3 +98,9 @@ class SocialLoginSerializerEx(SocialLoginSerializer):
} }
return social_login return social_login
class SocialAuthMethodSerializer(serializers.Serializer):
is_enabled = serializers.BooleanField(default=False)
icon = serializers.CharField(allow_blank=True, allow_null=True, required=False)
public_name = serializers.CharField()

@ -20,6 +20,7 @@ from cvat.apps.iam.views import (
google_oauth2_login as google_login, google_oauth2_login as google_login,
google_oauth2_callback as google_callback, google_oauth2_callback as google_callback,
LoginViewEx, GitHubLogin, GoogleLogin, LoginViewEx, GitHubLogin, GoogleLogin,
SocialAuthMethods,
) )
urlpatterns = [ urlpatterns = [
@ -39,6 +40,7 @@ if settings.IAM_TYPE == 'BASIC':
name='rest_password_reset_confirm'), name='rest_password_reset_confirm'),
path('password/change', PasswordChangeView.as_view(), path('password/change', PasswordChangeView.as_view(),
name='rest_password_change'), name='rest_password_change'),
path('social/methods/', SocialAuthMethods.as_view(), name='social_auth_methods'),
] ]
if allauth_settings.EMAIL_VERIFICATION != \ if allauth_settings.EMAIL_VERIFICATION != \
allauth_settings.EmailVerificationMethod.NONE: allauth_settings.EmailVerificationMethod.NONE:

@ -1,8 +1,9 @@
# Copyright (C) 2021-2022 Intel Corporation # Copyright (C) 2021-2023 Intel Corporation
# Copyright (C) 2022 CVAT.ai Corporation # Copyright (C) 2022 CVAT.ai Corporation
# #
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
import os.path as osp
import functools import functools
import hashlib import hashlib
@ -19,6 +20,7 @@ from django.views.decorators.http import etag as django_etag
from rest_framework.response import Response from rest_framework.response import Response
from dj_rest_auth.registration.views import RegisterView, SocialLoginView from dj_rest_auth.registration.views import RegisterView, SocialLoginView
from dj_rest_auth.views import LoginView from dj_rest_auth.views import LoginView
from dj_rest_auth.utils import import_callable
from allauth.account import app_settings as allauth_settings from allauth.account import app_settings as allauth_settings
from allauth.account.views import ConfirmEmailView from allauth.account.views import ConfirmEmailView
from allauth.account.utils import has_verified_email, send_email_confirmation from allauth.account.utils import has_verified_email, send_email_confirmation
@ -34,7 +36,18 @@ from drf_spectacular.contrib.rest_auth import get_token_serializer_class
from cvat.apps.iam.adapters import GitHubAdapter, GoogleAdapter from cvat.apps.iam.adapters import GitHubAdapter, GoogleAdapter
from .authentication import Signer from .authentication import Signer
from cvat.apps.iam.serializers import SocialLoginSerializerEx from cvat.apps.iam.serializers import SocialLoginSerializerEx, SocialAuthMethodSerializer
GitHubAdapter = (
import_callable(settings.SOCIALACCOUNT_GITHUB_ADAPTER)
if settings.USE_ALLAUTH_SOCIAL_ACCOUNTS
else None
)
GoogleAdapter = (
import_callable(settings.SOCIALACCOUNT_GOOGLE_ADAPTER)
if settings.USE_ALLAUTH_SOCIAL_ACCOUNTS
else None
)
def get_context(request): def get_context(request):
from cvat.apps.organizations.models import Organization, Membership from cvat.apps.organizations.models import Organization, Membership
@ -355,3 +368,51 @@ class GoogleLogin(SocialLoginViewEx):
adapter_class = GoogleAdapter adapter_class = GoogleAdapter
client_class = OAuth2Client client_class = OAuth2Client
callback_url = getattr(settings, 'GOOGLE_CALLBACK_URL', None) callback_url = getattr(settings, 'GOOGLE_CALLBACK_URL', None)
@extend_schema_view(
get=extend_schema(
summary='Method provides a list with integrated social accounts authentication.',
responses={
'200': OpenApiResponse(response=inline_serializer(
name="SocialAuthMethodsSerializer",
fields={
'google': SocialAuthMethodSerializer(),
'github': SocialAuthMethodSerializer(),
}
)),
}
)
)
class SocialAuthMethods(views.APIView):
serializer_class = SocialAuthMethodSerializer
permission_classes = [AllowAny]
authentication_classes = []
iam_organization_field = None
def get(self, request, *args, **kwargs):
use_social_auth = settings.USE_ALLAUTH_SOCIAL_ACCOUNTS
integrated_auth_providers = settings.SOCIALACCOUNT_PROVIDERS.keys() if use_social_auth else []
response = dict()
for provider in integrated_auth_providers:
icon = None
is_enabled = bool(
getattr(settings, f'SOCIAL_AUTH_{provider.upper()}_CLIENT_ID', None)
and getattr(settings, f'SOCIAL_AUTH_{provider.upper()}_CLIENT_SECRET', None)
)
icon_path = osp.join(settings.STATIC_ROOT, 'social_authentication', f'social-{provider}-logo.svg')
if is_enabled and osp.exists(icon_path):
with open(icon_path, 'r') as f:
icon = f.read()
serializer = SocialAuthMethodSerializer(data={
'is_enabled': is_enabled,
'icon': icon,
'public_name': settings.SOCIALACCOUNT_PROVIDERS[provider].get('PUBLIC_NAME', provider.title())
})
serializer.is_valid(raise_exception=True)
response[provider] = serializer.validated_data
return Response(response)

@ -632,6 +632,8 @@ ACCOUNT_LOGOUT_ON_PASSWORD_CHANGE = True
if USE_ALLAUTH_SOCIAL_ACCOUNTS: if USE_ALLAUTH_SOCIAL_ACCOUNTS:
SOCIALACCOUNT_ADAPTER = 'cvat.apps.iam.adapters.SocialAccountAdapterEx' SOCIALACCOUNT_ADAPTER = 'cvat.apps.iam.adapters.SocialAccountAdapterEx'
SOCIALACCOUNT_GITHUB_ADAPTER = 'cvat.apps.iam.adapters.GitHubAdapter'
SOCIALACCOUNT_GOOGLE_ADAPTER = 'cvat.apps.iam.adapters.GoogleAdapter'
SOCIALACCOUNT_LOGIN_ON_GET = True SOCIALACCOUNT_LOGIN_ON_GET = True
# It's required to define email in the case when a user has a private hidden email. # It's required to define email in the case when a user has a private hidden email.
# (e.g in github account set keep my email addresses private) # (e.g in github account set keep my email addresses private)
@ -660,7 +662,7 @@ if USE_ALLAUTH_SOCIAL_ACCOUNTS:
'SCOPE': [ 'profile', 'email', 'openid'], 'SCOPE': [ 'profile', 'email', 'openid'],
'AUTH_PARAMS': { 'AUTH_PARAMS': {
'access_type': 'online', 'access_type': 'online',
} },
}, },
'github': { 'github': {
'APP': { 'APP': {
@ -669,5 +671,10 @@ if USE_ALLAUTH_SOCIAL_ACCOUNTS:
'key': '' 'key': ''
}, },
'SCOPE': [ 'read:user', 'user:email' ], 'SCOPE': [ 'read:user', 'user:email' ],
# NOTE: Custom field. This is necessary for the user interface
# to render possible social account authentication option.
# If this field is not specified, then the option with the provider
# key with a capital letter will be used
'PUBLIC_NAME': 'GitHub',
}, },
} }

@ -11,11 +11,13 @@ description: 'Instructions on how to run all existence tests.'
1. Run CVAT instance: 1. Run CVAT instance:
``` ```
docker compose \ docker compose \
--env-file="tests/python/social_auth/.env" \
-f docker-compose.yml \ -f docker-compose.yml \
-f docker-compose.dev.yml \ -f docker-compose.dev.yml \
-f components/serverless/docker-compose.serverless.yml \ -f components/serverless/docker-compose.serverless.yml \
-f tests/docker-compose.minio.yml \ -f tests/docker-compose.minio.yml \
-f tests/docker-compose.file_share.yml up -d -f tests/docker-compose.file_share.yml \
-f tests/python/social_auth/docker-compose.yml up -d
``` ```
1. Add test user in CVAT: 1. Add test user in CVAT:
``` ```

@ -96,5 +96,36 @@ context('When clicking on the Logout button, get the user session closed.', () =
cy.url().should('include', '/auth/login'); cy.url().should('include', '/auth/login');
cy.closeNotification('.cvat-notification-notice-login-failed'); cy.closeNotification('.cvat-notification-notice-login-failed');
}); });
it('Login with Google and GitHub. Logout', () => {
let socialAuthMethods;
cy.request({
method: 'GET',
url: '/api/auth/social/methods/',
}).then((response) => {
socialAuthMethods = Object.keys(response.body);
expect(socialAuthMethods).length.gt(0);
cy.visit('auth/login');
cy.get('.cvat-social-authentication-icon').should('have.length', socialAuthMethods.length).within((items) => {
for (const item of items) {
expect(item.children.length).to.be.equal(1); // check that icon was received from the server
}
});
for (const provider of socialAuthMethods) {
let username = '';
cy.get(`.cvat-social-authentication-${provider}`).should('be.visible').click();
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.get('.cvat-right-header').should('exist').and('be.visible').within(() => {
cy.get('.cvat-header-menu-user-dropdown-user').should(($div) => {
username = $div.text();
});
}).then(() => {
cy.logout(username);
});
}
});
});
}); });
}); });

@ -0,0 +1,4 @@
GOOGLE_SERVER_PORT=4320
GOOGLE_SERVER_HOST="test-google"
GITHUB_SERVER_PORT=4321
GITHUB_SERVER_HOST="test-github"

@ -0,0 +1,35 @@
# Copyright (C) 2023 CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT
import os
from allauth.socialaccount.providers.github.views import GitHubOAuth2Adapter
from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter
from django.conf import settings
GOOGLE_SERVER_PORT = os.environ.get("GOOGLE_SERVER_PORT")
GOOGLE_SERVER_HOST = os.environ.get("GOOGLE_SERVER_HOST")
GITHUB_SERVER_PORT = os.environ.get("GITHUB_SERVER_PORT")
GITHUB_SERVER_HOST = os.environ.get("GITHUB_SERVER_HOST")
class TestGitHubAdapter(GitHubOAuth2Adapter):
access_token_url = (
f"http://{GITHUB_SERVER_HOST}:{GITHUB_SERVER_PORT}/login/oauth/access_token" # nosec
)
authorize_url = f"http://localhost:{GITHUB_SERVER_PORT}/login/oauth/authorize"
profile_url = f"http://{GITHUB_SERVER_HOST}:{GITHUB_SERVER_PORT}/user"
emails_url = f"http://{GITHUB_SERVER_HOST}:{GITHUB_SERVER_PORT}/user/emails"
def get_callback_url(self, request, app):
return settings.GITHUB_CALLBACK_URL
class TestGoogleAdapter(GoogleOAuth2Adapter):
access_token_url = f"http://{GOOGLE_SERVER_HOST}:{GOOGLE_SERVER_PORT}/o/oauth2/token"
authorize_url = f"http://localhost:{GOOGLE_SERVER_PORT}/o/oauth2/auth"
profile_url = f"http://{GOOGLE_SERVER_HOST}:{GOOGLE_SERVER_PORT}/oauth2/v1/userinfo"
def get_callback_url(self, request, app):
return settings.GOOGLE_CALLBACK_URL

@ -0,0 +1,47 @@
services:
cvat_server:
environment:
USE_ALLAUTH_SOCIAL_ACCOUNTS: "True"
SOCIAL_AUTH_GOOGLE_CLIENT_ID: "XXX"
SOCIAL_AUTH_GOOGLE_CLIENT_SECRET: "XXX"
SOCIAL_AUTH_GITHUB_CLIENT_ID: "XXX"
SOCIAL_AUTH_GITHUB_CLIENT_SECRET: "XXX"
DJANGO_SETTINGS_MODULE: social_auth.settings
GOOGLE_SERVER_HOST:
GOOGLE_SERVER_PORT:
GITHUB_SERVER_HOST:
GITHUB_SERVER_PORT:
volumes:
- ./tests/python/social_auth:/home/django/social_auth:ro
google_auth_server:
image: python:3.9-slim
restart: always
environment:
GOOGLE_SERVER_HOST:
GOOGLE_SERVER_PORT:
ports:
- '${GOOGLE_SERVER_PORT}:${GOOGLE_SERVER_PORT}'
command: python3 /tmp/server.py --server "google"
volumes:
- ./tests/python/social_auth:/tmp
networks:
cvat:
aliases:
- ${GOOGLE_SERVER_HOST}
github_auth_server:
image: python:3.9-slim
restart: always
environment:
GITHUB_SERVER_HOST:
GITHUB_SERVER_PORT:
ports:
- '${GITHUB_SERVER_PORT}:${GITHUB_SERVER_PORT}'
command: python3 /tmp/server.py --server "github"
volumes:
- ./tests/python/social_auth:/tmp
networks:
cvat:
aliases:
- ${GITHUB_SERVER_HOST}

@ -0,0 +1,217 @@
# Copyright (C) 2023 CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT
import argparse
import json
import os
import string
from abc import ABC, abstractmethod
from datetime import datetime
from http.server import BaseHTTPRequestHandler, HTTPServer
from random import choice, random, sample
from urllib.parse import parse_qsl, urlparse
class CommonRequestHandlerClass(BaseHTTPRequestHandler, ABC):
def _set_headers(self):
self.send_response(406)
self.send_header("Content-type", "text/html")
self.end_headers()
self.wfile.write(f"Unsupported request. Path: {self.path}".encode("utf8"))
def get_profile(self, token=None):
if not token:
self.send_response(403)
self.end_headers()
return
self.send_response(200)
self.end_headers()
self.wfile.write(json.dumps(self.PROFILE).encode("utf-8"))
@abstractmethod
def authorize(self, query_params):
pass
@abstractmethod
def generate_access_token(self):
pass
def check_query(self, query_params):
supported_response_type = "code"
if not "client_id" in query_params:
self.send_response(400)
self.wfile.write("Client id not found in query params".encode("utf8"))
return
if not "redirect_uri" in query_params:
self.send_response(400)
self.wfile.write("Redirect uri not found".encode("utf8"))
return
if query_params.get("response_type", "code") != supported_response_type:
self.send_response(400)
self.wfile.write(
"Only code response type is supported by dummy auth server".encode("utf8")
)
return
def do_GET(self):
u = urlparse(self.path)
if u.path == self.AUTHORIZE_PATH:
return self.authorize(dict(parse_qsl(u.query)))
elif u.path == self.PROFILE_PATH:
token = self.headers.get("Authorization") or dict(parse_qsl(u.query)).get(
"access_token"
)
return self.get_profile(token)
self._set_headers()
def do_POST(self):
u = urlparse(self.path)
if u.path == self.TOKEN_PATH:
return self.generate_access_token()
self._set_headers()
class GithubRequestHandlerClass(CommonRequestHandlerClass):
AUTHORIZE_PATH = "/login/oauth/authorize"
PROFILE_PATH = "/user"
TOKEN_PATH = "/login/oauth/access_token"
CODE_LENGTH = 20
AUTH_TOKEN_LENGTH = 40
LOGIN = "test-user"
UID = int(random() * 100)
# demo profile not including all information returned by github
PROFILE = {
"login": LOGIN,
"id": UID,
"avatar_url": f"https://avatars.github.com/u/{UID}",
"url": f"https://api.github.com/users/{LOGIN}",
"html_url": f"https://github.com/{LOGIN}",
"type": "User",
"site_admin": False,
"name": "Test User",
"location": "Germany, Munich",
"email": "github.user@test.com",
"hireable": None,
"created_at": str(datetime.now()),
"updated_at": str(datetime.now()),
"two_factor_authentication": False,
}
def authorize(self, query_params):
super().check_query(query_params)
self.send_response(302)
redirect_to = query_params["redirect_uri"]
generated_code = "".join(sample(string.ascii_lowercase + string.digits, self.CODE_LENGTH))
# add query params
new_query = (
f"?code={generated_code}&state={query_params['state']}&"
f"scope={query_params['scope']}&promt=none"
)
redirect_to += new_query
self.send_header("Location", redirect_to)
self.send_header("Content-type", "text/html")
self.end_headers()
def generate_access_token(self):
self.send_response(200)
self.send_header("Content-type", "application/x-www-form-urlencoded; charset=utf-8")
generated_token = "".join(
sample(string.ascii_letters + string.digits, self.AUTH_TOKEN_LENGTH)
)
scope = "read:user,user:email"
content = f"access_token={generated_token}&scope={scope}&token_type=bearer".encode("utf-8")
self.end_headers()
self.wfile.write(content)
class GoogleRequestHandlerClass(CommonRequestHandlerClass):
AUTHORIZE_PATH = "/o/oauth2/auth"
PROFILE_PATH = "/oauth2/v1/userinfo"
TOKEN_PATH = "/o/oauth2/token"
CODE_LENGTH = 70 # in real case 256 bytes
AUTH_TOKEN_LENGTH = 100 # in real case 2048 bytes
UID = int(random() * 100)
# demo profile not including all information returned by google
PROFILE = {
"id": UID,
"email": "google.user@gmail.com",
"verified_email": True,
"name": "Test User",
"given_name": "Test",
"family_name": "User",
"picture": f"https://avatars.google.com/u/{UID}",
"locale": "en",
}
def authorize(self, query_params):
super().check_query(query_params)
self.send_response(302)
redirect_to = query_params["redirect_uri"]
symbols = string.ascii_letters + string.digits
generated_code = "".join([choice(symbols) for i in range(self.CODE_LENGTH)])
# add query params
new_query = (
f"?code={generated_code}&state={query_params['state']}&"
f"scope={query_params['scope']}&promt=none"
)
redirect_to += new_query
self.send_header("Location", redirect_to)
self.send_header("Content-type", "text/html")
self.end_headers()
def generate_access_token(self):
self.send_response(200)
self.send_header("Content-type", "application/json; charset=utf-8")
symbols = string.ascii_letters + string.digits + string.punctuation
generated_token = "".join([choice(symbols) for i in range(self.AUTH_TOKEN_LENGTH)])
id_token = "".join([choice(symbols) for i in range(self.AUTH_TOKEN_LENGTH)])
scope = "https://www.googleapis.com/auth/userinfo.profile openid https://www.googleapis.com/auth/userinfo.email"
content = {
"access_token": generated_token,
"expires_in": 3600, # 1 h
"scope": scope,
"token_type": "Bearer",
"id_token": id_token,
}
self.end_headers()
self.wfile.write(json.dumps(content).encode("utf-8"))
class AuthServer:
SERVER_HOST = "0.0.0.0"
def run(self):
print(f"Starting dummy authentication server on {self.SERVER_HOST}, {self.SERVER_PORT}")
HTTPServer((self.SERVER_HOST, self.SERVER_PORT), self.REQUEST_HANDLER_CLASS).serve_forever()
class GoogleAuthServer(AuthServer):
SERVER_PORT = int(os.environ.get("GOOGLE_SERVER_PORT", "4320"))
REQUEST_HANDLER_CLASS = GoogleRequestHandlerClass
class GithubAuthServer(AuthServer):
SERVER_PORT = int(os.environ.get("GITHUB_SERVER_PORT", "4321"))
REQUEST_HANDLER_CLASS = GithubRequestHandlerClass
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--server", choices=["google", "github"], type=str, default="google")
server = parser.parse_args().server
auth_servers = {
"google": GoogleAuthServer,
"github": GithubAuthServer,
}
server_class = auth_servers[server]
server_class().run()

@ -0,0 +1,11 @@
# Copyright (C) 2023 CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT
from cvat.settings.production import *
ACCOUNT_EMAIL_REQUIRED = True
if USE_ALLAUTH_SOCIAL_ACCOUNTS:
SOCIALACCOUNT_GITHUB_ADAPTER = "social_auth.adapters.TestGitHubAdapter"
SOCIALACCOUNT_GOOGLE_ADAPTER = "social_auth.adapters.TestGoogleAdapter"
Loading…
Cancel
Save