Skip to content

Commit

Permalink
Feature/display name backend for selector (#1058)
Browse files Browse the repository at this point in the history
* add uma2 test and service

* add new graphql for updating display name

* add the enforcement on the list extension

* integrate into frontend

* upd unit tests

* add graphql whitelist query

* fix for no namespace on name

* return displayName from currentNamespace query

---------

Co-authored-by: Russell Vinegar <russell.vinegar@gov.bc.ca>
  • Loading branch information
ikethecoder and rustyjux committed Jun 4, 2024
1 parent 5dde66f commit 0217134
Show file tree
Hide file tree
Showing 12 changed files with 346 additions and 16 deletions.
9 changes: 8 additions & 1 deletion .env.local
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
LOG_LEVEL=info
LOG_LEVEL=debug
DISABLE_LOGGING='true'
AUTH_STRATEGY=Oauth2Proxy
KNEX_HOST=kong-db
Expand Down Expand Up @@ -31,3 +31,10 @@ NEXT_PUBLIC_DEVELOPER_IDS=idir,bceid,bcsc,github
NEXT_PUBLIC_PROVIDER_IDS=idir
NEXT_PUBLIC_ACCOUNT_BCEID_URL=https://www.test.bceid.ca/logon.aspx?returnUrl=/profile_management
NEXT_PUBLIC_ACCOUNT_BCSC_URL=https://idtest.gov.bc.ca/account/

# For automated integrated testing
TEST_PORTAL_CLIENT_ID=aps-portal
TEST_PORTAL_CLIENT_SECRET=8e1a17ed-cb93-4806-ac32-e303d1c86018
TEST_PORTAL_USERNAME=janis@idir
TEST_PORTAL_PASSWORD=awsummer

2 changes: 1 addition & 1 deletion .github/workflows/ci-feat-sonar.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:

- name: Run Tests
run: |
docker compose up kong-db -d
docker compose up keycloak -d
set -o allexport
source ./.env.local
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

mutation UpdateNamespaceDisplayName($displayName: String!) {
updateCurrentNamespaceDisplayName(displayName: $displayName)
}
1 change: 1 addition & 0 deletions src/authz/matrix.csv
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ API Owner Role Rules,,forceDeleteNamespace,,,,,,,api-owner,,,allow,
API Owner Role Rules,,namespace,,,,,,,api-owner,,,allow,
API Owner Role Rules,,currentNamespace,,,,,,,,,"portal-user,api-owner,provider-user,access-manager,credential-admin",allow,
API Owner / Provider Role Rules,,updateCurrentNamespace,,,,,,,api-owner,,,allow,
API Owner / Provider Role Rules,,updateCurrentNamespaceDisplayName,,,,,,,api-owner,,,allow,
API Owner Role Rules,,updatePermissions,,,,,,,api-owner,,,allow,
API Owner Role Rules,,grantPermissions,,,,,,,api-owner,,,allow,
API Owner Role Rules,,revokePermissions,,,,,,,api-owner,,,allow,
Expand Down
35 changes: 35 additions & 0 deletions src/lists/extensions/Namespace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ module.exports = {
const resource: any = await getResource(selectedNS, envCtx);
merged['id'] = resource['id'];
merged['scopes'] = resource['scopes'];
merged['displayName'] = resource['displayName'];
}

if (merged.org) {
Expand Down Expand Up @@ -341,6 +342,39 @@ module.exports = {
return true;
},
},
{
schema:
'updateCurrentNamespaceDisplayName(displayName: String): String',
resolver: async (
item: any,
{ displayName }: any,
context: any,
info: any,
{ query, access }: any
): Promise<boolean> => {
if (
context.req.user?.namespace == null ||
typeof context.req.user?.namespace === 'undefined'
) {
return null;
}

const ns = context.req.user?.namespace;

const prodEnv = await getGwaProductEnvironment(context, true);

await getNamespaceResourceSets(prodEnv); // sets accessToken

const resourcesApi = new UMAResourceRegistrationService(
prodEnv.uma2.resource_registration_endpoint,
prodEnv.accessToken
);

await resourcesApi.updateDisplayName(ns, displayName);
return true;
},
access: EnforcementPoint,
},
{
schema:
'updateCurrentNamespace(org: String, orgUnit: String): String',
Expand Down Expand Up @@ -416,6 +450,7 @@ module.exports = {
});
}
},
access: EnforcementPoint,
},
{
schema:
Expand Down
23 changes: 17 additions & 6 deletions src/nextapp/components/edit-display-name/edit-display-name.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ const EditNamespaceDisplayName: React.FC<EditNamespaceDisplayNameProps> = ({
const queryClient = useQueryClient();
const mutate = useApiMutation(mutation);
const [inputValue, setInputValue] = React.useState(data.displayName || '');
const [charCount, setCharCount] = React.useState(data.displayName?.length || 0);
const [charCount, setCharCount] = React.useState(
data.displayName?.length || 0
);
const charLimit = 30;
const handleInputChange = (event) => {
const { value } = event.target;
Expand All @@ -49,8 +51,16 @@ const EditNamespaceDisplayName: React.FC<EditNamespaceDisplayNameProps> = ({
event.preventDefault();
if (charCount <= charLimit) {
updateNamespaceDisplayName();
submitTheForm();
}
};
const submitTheForm = async () => {
toast({
title: 'Submitted it!',
status: 'success',
isClosable: true,
});
};
const updateNamespaceDisplayName = async () => {
if (form.current) {
try {
Expand Down Expand Up @@ -99,11 +109,12 @@ const EditNamespaceDisplayName: React.FC<EditNamespaceDisplayNameProps> = ({
<form ref={form} onSubmit={handleSubmit}>
<FormControl isRequired>
<FormLabel></FormLabel>
<FormHelperText pb={4} fontSize={"16px"}>
A meaningful display name makes it easy for anyone to identify and distinguish this Gateway from others.
<FormHelperText pb={4} fontSize={'16px'}>
A meaningful display name makes it easy for anyone to identify
and distinguish this Gateway from others.
</FormHelperText>
<Text
fontSize={"12px"}
fontSize={'12px'}
color={charCount > charLimit ? 'bc-error' : 'gray.500'}
mt={2}
textAlign="right"
Expand Down Expand Up @@ -155,7 +166,7 @@ const EditNamespaceDisplayName: React.FC<EditNamespaceDisplayNameProps> = ({
export default EditNamespaceDisplayName;

const mutation = gql`
mutation UpdateCurrentNamespace($displayName: String!) {
updateCurrentNamespace(displayName: $displayName)
mutation UpdateNamespaceDisplayName($displayName: String!) {
updateCurrentNamespaceDisplayName(displayName: $displayName)
}
`;
20 changes: 15 additions & 5 deletions src/nextapp/pages/manager/namespaces/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -168,27 +168,35 @@ const NamespacesPage: React.FC = () => {
}, [client, mutate, router, toast, user]);
const title = (
<>

{(namespace.isFetching || namespace.isLoading) && (
<Skeleton width="400px" height="20px" mt={4} />
)}
{namespace.isSuccess && !namespace.isFetching && (
<>
<Flex align="center" gridGap={4}>
{namespace.data?.currentNamespace?.displayName}
<EditNamespaceDisplayName data={namespace.data?.currentNamespace} queryKey={queryKey} />
<EditNamespaceDisplayName
data={namespace.data?.currentNamespace}
queryKey={queryKey}
/>
{namespace.data?.currentNamespace?.orgEnabled && (
<Tooltip
hasArrow
label={`${user.namespace} is enabled to publish APIs to the directory`}
>
<Box display="flex">
<Icon as={FaCheckCircle} color="bc-success" boxSize="0.65em" />
<Icon
as={FaCheckCircle}
color="bc-success"
boxSize="0.65em"
/>
</Box>
</Tooltip>
)}
</Flex>
<Text fontSize="xl" pt={1}>{namespace?.data.currentNamespace.name}</Text>
<Text fontSize="xl" pt={1}>
{namespace?.data.currentNamespace?.name}
</Text>
<Flex align="center" mt={4}>
<Text
color={currentOrg.color}
Expand Down Expand Up @@ -245,7 +253,9 @@ const NamespacesPage: React.FC = () => {
<Head>
<title>
API Program Services | Namespaces
{hasNamespace ? ` | ${namespace.data?.currentNamespace?.displayName}` : ''}
{hasNamespace
? ` | ${namespace.data?.currentNamespace?.displayName}`
: ''}
</title>
</Head>
<ApproveBanner />
Expand Down
1 change: 1 addition & 0 deletions src/services/keycloak/namespace-details.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export async function getAllNamespaces(envCtx: EnvironmentContext) {
const nsList = namespaces.map((ns: ResourceSet) => ({
id: ns.id,
name: ns.name,
displayName: ns.displayName,
scopes: ns.resource_scopes,
prodEnvId: envCtx.prodEnv.id,
}));
Expand Down
23 changes: 21 additions & 2 deletions src/services/keycloak/token-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,33 @@ export class KeycloakTokenService {
public async getKeycloakSession(
clientId: string,
clientSecret: string
): Promise<string> {
return this.getKeycloakSessionByGrant(
clientId,
clientSecret,
'client_credentials'
);
}

public async getKeycloakSessionByGrant(
clientId: string,
clientSecret: string,
grantType: string,
username?: string,
password?: string
): Promise<string> {
const params = new URLSearchParams();
params.append('grant_type', 'client_credentials');
params.append('grant_type', grantType);
params.append('client_id', clientId);
params.append('client_secret', clientSecret);
if (username) {
params.append('username', username);
params.append('password', password);
}

logger.debug(
'[getKeycloakSession] Using %s for endpoint %s',
'[getKeycloakSession] Using %s %s for endpoint %s',
grantType,
clientId,
this.tokenUrl
);
Expand Down
57 changes: 56 additions & 1 deletion src/services/uma2/resource-registration-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import fetch from 'node-fetch';
import { Logger } from '../../logger';
import querystring from 'querystring';
import { headers } from '../keycloak/keycloak-api';
import { regExprValidation } from '../utils';

const logger = Logger('uma2-resource');

Expand Down Expand Up @@ -48,6 +49,18 @@ export interface ResourceSetInput {
ownerManagedAccess: boolean;
}

export interface ResourceSetUpdateInput {
_id: string;
name: string;
displayName: string;
type: string;
uris: string[];
icon_uri: string;
scopes: string[];
owner: ResourceOwner;
ownerManagedAccess: boolean;
}

export class UMAResourceRegistrationService {
private resourceRegistrationEndpoint: string;
private accessToken: string;
Expand All @@ -72,14 +85,56 @@ export class UMAResourceRegistrationService {
return result;
}

async updateResourceSet(set: ResourceSetUpdateInput) {
const url = `${this.resourceRegistrationEndpoint}/${set._id}`;
logger.debug('[updateResourceSet] URL %s', url);
logger.debug('[updateResourceSet] %j', set);
const result = await fetch(url, {
method: 'put',
body: JSON.stringify(set),
headers: headers(this.accessToken) as any,
}).then(checkStatus);
logger.debug('[updateResourceSet] (%s) [%j] OK', set._id, result.status);
}

public async updateDisplayName(name: string, displayName: string) {
const displayNameValidationRule = '^[A-Za-z0-9-()_ ]{0,50}$';

regExprValidation(
displayNameValidationRule,
displayName,
'Display name can not be longer than 50 characters and can only use special characters "-()_ ".'
);

const before = await this.findResourceByName(name);

await this.updateResourceSet({
_id: before.id,
name: before.name,
displayName,
type: before.type,
uris: before.uris,
icon_uri: before.icon_uri,
scopes: before.resource_scopes.map((s) => s.name),
owner: before.owner,
ownerManagedAccess: before.ownerManagedAccess,
});

// need a small pause here, otherwise keycloak
// gives a 'reason: socket hang up' on next call to it
await new Promise((resolve) => {
setTimeout(resolve, 100);
});
}

public async deleteResourceSet(rid: string) {
const url = `${this.resourceRegistrationEndpoint}/${rid}`;
logger.debug('[deleteResourceSet] URL %s', url);
const result = await fetch(url, {
method: 'delete',
headers: headers(this.accessToken) as any,
}).then(checkStatus);
logger.debug('[deleteResourceSet] (%s) OK', rid);
logger.debug('[deleteResourceSet] (%s) [%j] OK', rid, result.status);
}

public async getResourceSet(rid: string): Promise<ResourceSet> {
Expand Down
Loading

0 comments on commit 0217134

Please sign in to comment.