Skip to content
This repository has been archived by the owner on Oct 20, 2023. It is now read-only.

Commit

Permalink
Update global operators graphql query & mutation auth (#58)
Browse files Browse the repository at this point in the history
* Add custom auth middleware for global operator graphql query and mutation
Update login form with feedback that creating a new user is disabled
Closes #55
  • Loading branch information
GoldingAustin authored Jan 6, 2023
1 parent b4d087e commit 8093fac
Show file tree
Hide file tree
Showing 6 changed files with 148 additions and 111 deletions.
14 changes: 13 additions & 1 deletion applications/client/src/components/Forms/LoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ const isDevelop = import.meta.env.DEV;

export const LoginForm = observer<LoginFormProps>(({ onSubmit, submitText = 'Login', ...props }) => {
const store = useStore();
const { data, refetch } = useQuery(['users'], async () => await store.graphqlStore.queryGlobalOperators({}));
const state = createState({
username: isDevelop ? store.auth.userName || 'dev' : store.auth.userName || '',
password: '',
passwordFocus: false,
loading: false,
errorMessage: '',
*handleSubmit(event: FormEvent<HTMLFormElement>) {
Expand Down Expand Up @@ -65,6 +65,14 @@ export const LoginForm = observer<LoginFormProps>(({ onSubmit, submitText = 'Log
},
});

const { data, refetch } = useQuery(
['users', state.password],
async () => await store.graphqlStore.queryGlobalOperators({ password: state.password }),
{
enabled: !!state.password && !state.passwordFocus,
}
);

return (
<form {...props} onSubmit={state.handleSubmit} autoComplete="on">
{!store.appMeta.blueTeam && (
Expand All @@ -78,6 +86,8 @@ export const LoginForm = observer<LoginFormProps>(({ onSubmit, submitText = 'Log
name="password"
placeholder="server password"
css={inputSpacingTightStyle}
onFocus={() => state.update('passwordFocus', true)}
onBlur={() => state.update('passwordFocus', false)}
leftIcon={<CarbonIcon icon={Password16} />}
large
/>
Expand All @@ -86,7 +96,9 @@ export const LoginForm = observer<LoginFormProps>(({ onSubmit, submitText = 'Log
<UsernameInput
cy-test="username"
username={state.username}
password={state.password}
refetch={refetch}
disableCreateUser={!data}
users={data?.globalOperators}
updateUser={(userName) => state.update('username', userName)}
css={otherSpacingLooseStyle}
Expand Down
187 changes: 98 additions & 89 deletions applications/client/src/components/Forms/UsernameInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,111 +7,120 @@ import { CarbonIcon, createState, escapeRegExpChars } from '@redeye/client/compo
import type { GlobalOperatorModel } from '@redeye/client/store';
import { useStore } from '@redeye/client/store';
import { Styles, TokensAll, Txt } from '@redeye/ui-styles';
import { useMutation } from '@tanstack/react-query';
import { observer } from 'mobx-react-lite';

type UsernameInputProps = Omit<
SuggestProps<GlobalOperatorModel>,
'itemRenderer' | 'items' | 'inputValueRenderer' | 'onItemSelect'
> & {
username: string;
password: string;
disableCreateUser: boolean;
refetch: () => any;
users?: GlobalOperatorModel[];
updateUser: (userName) => void;
};

export const UsernameInput = observer<UsernameInputProps>(({ username, refetch, users = [], updateUser, ...props }) => {
const store = useStore();
const state = createState({
active: null as null | GlobalOperatorModel,
query: username,
async addUser() {
try {
const op = await store.graphqlStore.mutateCreateGlobalOperator({
username: this.query,
});
if (op?.createGlobalOperator) {
refetch();
updateUser(this.query);
}
} catch (e) {
window.console.debug(e);
}
},
});
export const UsernameInput = observer<UsernameInputProps>(
({ username, password, disableCreateUser, refetch, users = [], updateUser, ...props }) => {
const store = useStore();

return (
<ClassNames>
{({ css: classCss }) => (
// for the popoverProps.className
<Suggest2
cy-test="username"
openOnKeyDown
query={state.query}
createNewItemFromQuery={(query) => ({ name: query, id: query } as any)}
selectedItem={
store.graphqlStore.globalOperators.get(state.query) ||
({ name: state.query, id: state.query } as GlobalOperatorModel)
}
itemPredicate={filterUsers}
activeItem={state.active || getCreateNewItem()}
onItemSelect={(item) => {
state.update('query', item?.name);
updateUser(item.name);
}}
onActiveItemChange={(active) => state.update('active', active)}
onQueryChange={(query) => state.update('query', query)}
items={users || []}
inputValueRenderer={(item) => item.name as string}
css={menuParentStyle}
fill
popoverProps={
{
minimal: true,
popoverClassName: classCss(menuParentStyle),
} as any
const state = createState({
active: null as null | GlobalOperatorModel,
query: username,
});

const { mutate: addUser } = useMutation(
async () =>
await store.graphqlStore.mutateCreateGlobalOperator({
username: state.query,
password,
}),
{
onSuccess(op) {
if (op?.createGlobalOperator) {
refetch();
updateUser(state.query);
}
inputProps={{
value: state.query,
onBlur: () => updateUser(state.query),
type: 'text',
name: 'username',
autoComplete: 'username',
placeholder: 'user',
leftIcon: <CarbonIcon icon={User16} />,
large: true,
fill: true,
}}
itemRenderer={(user, { handleClick, modifiers }) => {
if (!modifiers.matchesPredicate) return null;
return (
},
}
);

return (
<ClassNames>
{({ css: classCss }) => (
// for the popoverProps.className
<Suggest2
cy-test="username"
openOnKeyDown
query={state.query}
createNewItemFromQuery={(query) => ({ name: query, id: query } as any)}
selectedItem={
store.graphqlStore.globalOperators.get(state.query) ||
({ name: state.query, id: state.query } as GlobalOperatorModel)
}
itemPredicate={filterUsers}
activeItem={state.active || getCreateNewItem()}
onItemSelect={(item) => {
state.update('query', item?.name);
updateUser(item.name);
}}
onActiveItemChange={(active) => state.update('active', active)}
onQueryChange={(query) => state.update('query', query)}
items={users || []}
inputValueRenderer={(item) => item.name as string}
css={menuParentStyle}
fill
popoverProps={
{
minimal: true,
popoverClassName: classCss(menuParentStyle),
} as any
}
inputProps={{
value: state.query,
onBlur: () => updateUser(state.query),
type: 'text',
name: 'username',
autoComplete: 'username',
placeholder: 'user',
leftIcon: <CarbonIcon icon={User16} />,
large: true,
fill: true,
}}
itemRenderer={(user, { handleClick, modifiers }) => {
if (!modifiers.matchesPredicate) return null;
return (
<MenuItem
text={highlightText(user.name as string, state.query)}
active={modifiers.active}
disabled={modifiers.disabled}
// label={user.campaign} // TODO: add campaign the user comes from
key={user.name}
onClick={handleClick}
/>
);
}}
createNewItemRenderer={(_, isActive: boolean) => (
<MenuItem
text={highlightText(user.name as string, state.query)}
active={modifiers.active}
disabled={modifiers.disabled}
// label={user.campaign} // TODO: add campaign the user comes from
key={user.name}
onClick={handleClick}
icon={<CarbonIcon icon={Add16} />}
text="New User"
disabled={disableCreateUser || store.graphqlStore.globalOperators.has(state.query)}
label={state.query}
active={isActive}
onClick={() => addUser()}
shouldDismissPopover={false}
css={newUserStyle}
/>
);
}}
createNewItemRenderer={(_, isActive: boolean) => (
<MenuItem
icon={<CarbonIcon icon={Add16} />}
text="New User"
disabled={store.graphqlStore.globalOperators.has(state.query)}
label={state.query}
active={isActive}
onClick={state.addUser}
shouldDismissPopover={false}
css={newUserStyle}
/>
)}
{...props}
/>
)}
</ClassNames>
);
});
)}
{...props}
/>
)}
</ClassNames>
);
}
);

const menuParentStyle = css`
.${Classes.MENU} {
Expand Down
29 changes: 15 additions & 14 deletions applications/client/src/store/graphql/RootStore.base.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
/* This is a mk-gql generated file, don't modify it manually */
import type { MKGQLStore, QueryOptions } from 'mk-gql';
import { createMKGQLStore } from 'mk-gql';
/* eslint-disable */
/* tslint:disable */
// @ts-nocheck
Expand All @@ -14,27 +16,26 @@ import {
prop,
Ref,
} from 'mobx-keystone';
import { createMKGQLStore, MKGQLStore, QueryOptions } from 'mk-gql';
import { MergeHelper } from './mergeHelper';

import { AnnotationModel, annotationModelPrimitives, AnnotationModelSelector } from './AnnotationModel';
import { BeaconModel, beaconModelPrimitives, BeaconModelSelector } from './BeaconModel';
import { BeaconMetaModel } from './BeaconMetaModel';
import { BeaconModel, beaconModelPrimitives, BeaconModelSelector } from './BeaconModel';
import { CampaignModel, campaignModelPrimitives, CampaignModelSelector } from './CampaignModel';
import { CommandModel, commandModelPrimitives, CommandModelSelector } from './CommandModel';
import { CommandGroupModel, commandGroupModelPrimitives, CommandGroupModelSelector } from './CommandGroupModel';
import { CommandModel, commandModelPrimitives, CommandModelSelector } from './CommandModel';
import {
CommandTypeCountModel,
commandTypeCountModelPrimitives,
CommandTypeCountModelSelector,
} from './CommandTypeCountModel';
import { FileModel, fileModelPrimitives, FileModelSelector } from './FileModel';
import { GlobalOperatorModel, globalOperatorModelPrimitives, GlobalOperatorModelSelector } from './GlobalOperatorModel';
import { HostModel, hostModelPrimitives, HostModelSelector } from './HostModel';
import { HostMetaModel } from './HostMetaModel';
import { HostModel, hostModelPrimitives, HostModelSelector } from './HostModel';
import { ImageModel, imageModelPrimitives, ImageModelSelector } from './ImageModel';
import { LinkModel, linkModelPrimitives, LinkModelSelector } from './LinkModel';
import { LogEntryModel, logEntryModelPrimitives, LogEntryModelSelector } from './LogEntryModel';
import { MergeHelper } from './mergeHelper';
import { OperatorModel, operatorModelPrimitives, OperatorModelSelector } from './OperatorModel';
import {
ParsingProgressModel,
Expand All @@ -47,16 +48,16 @@ import {
presentationItemModelPrimitives,
PresentationItemModelSelector,
} from './PresentationItemModel';
import { ServerModel, serverModelPrimitives, ServerModelSelector } from './ServerModel';
import { ServerMetaModel } from './ServerMetaModel';
import { ServerModel, serverModelPrimitives, ServerModelSelector } from './ServerModel';
import { ServerParsingProgressModel } from './ServerParsingProgressModel';
import { TagModel, tagModelPrimitives, TagModelSelector } from './TagModel';
import { TimelineModel, timelineModelPrimitives, TimelineModelSelector } from './TimelineModel';
import { TimelineBucketModel } from './TimelineBucketModel';
import { TimelineCommandCountTupleModel } from './TimelineCommandCountTupleModel';
import type { ServerType } from './ServerTypeEnum';
import type { SortDirection } from './SortDirectionEnum';
import type { SortOption } from './SortOptionEnum';
import { TagModel, tagModelPrimitives, TagModelSelector } from './TagModel';
import { TimelineBucketModel } from './TimelineBucketModel';
import { TimelineCommandCountTupleModel } from './TimelineCommandCountTupleModel';
import { TimelineModel, timelineModelPrimitives, TimelineModelSelector } from './TimelineModel';

export type AnonymizationInput = {
findReplace?: FindReplaceInput[];
Expand Down Expand Up @@ -467,7 +468,7 @@ export class RootStoreBase extends ExtendedModel(
}
// Get all the operators for all campaigns
@modelAction queryGlobalOperators(
variables?: {},
variables: { password: string },
resultSelector:
| string
| ((
Expand All @@ -477,7 +478,7 @@ export class RootStoreBase extends ExtendedModel(
clean?: boolean
) {
return this.query<{ globalOperators: GlobalOperatorModel[] }>(
`query globalOperators { globalOperators {
`query globalOperators($password: String!) { globalOperators(password: $password) {
${
typeof resultSelector === 'function' ? resultSelector(GlobalOperatorModelSelector).toString() : resultSelector
}
Expand Down Expand Up @@ -845,7 +846,7 @@ export class RootStoreBase extends ExtendedModel(
}
// Create a global user
@modelAction mutateCreateGlobalOperator(
variables: { username: string },
variables: { password: string; username: string },
resultSelector:
| string
| ((
Expand All @@ -854,7 +855,7 @@ export class RootStoreBase extends ExtendedModel(
optimisticUpdate?: () => void
) {
return this.mutate<{ createGlobalOperator: GlobalOperatorModel }>(
`mutation createGlobalOperator($username: String!) { createGlobalOperator(username: $username) {
`mutation createGlobalOperator($password: String!, $username: String!) { createGlobalOperator(password: $password, username: $username) {
${
typeof resultSelector === 'function' ? resultSelector(GlobalOperatorModelSelector).toString() : resultSelector
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ describe('Query Global Operators', () => {

cy.selectCampaign(camp);

const query = `query globalOperators {
globalOperators {
const query = `query globalOperators($password: String!) {
globalOperators(password: $password) {
id
name
}
}`;
graphqlRequest(query).then((res) => {
const variables = `{"password": "937038570"}`;
graphqlRequest(query, variables).then((res) => {
expect(res.body.data.globalOperators[0].id).to.eq('analyst');
expect(res.body.data.globalOperators[0].name).to.eq('analyst');
});
Expand Down
Loading

0 comments on commit 8093fac

Please sign in to comment.