Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PIMS-669: User & Roles Services #2037

Merged
merged 18 commits into from
Feb 2, 2024
Merged

Conversation

GrahamS-Quartech
Copy link
Contributor

@GrahamS-Quartech GrahamS-Quartech commented Jan 17, 2024

🎯 Summary

PIMS-669

  • Implemented services for users, admin-users, and admin-roles matching the original .NET functionality
  • Made some helper functions and interfaces for parsing out different kinds of keycloak users and looking them up as internal users
  • Made some changes to the typeorm entities so that you can join to agencies and roles from users and join to claims from roles. Note some of the syntax I've used here, import type { } must be used to avoid circular dependencies when defining many to many relations in different files.
  • Added a script to the package.json to interface with the typeorm cli
  • Added basic unit tests for the relevant controllers and services.

🔰 Checklist

  • I have read and agree with the following checklist and am following the guidelines in our Code of Conduct document.
  • I have performed a self-review of my code.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have made corresponding changes to the documentation where required.
  • I have tested my changes to the best of my ability.
  • My changes generate no new warnings.

… fully implement the join tables between users-roles, users-agencies, and roles-claims, allowing easy manipulation in the query builders
@GrahamS-Quartech
Copy link
Contributor Author

There's a lot to look through in terms of review here, but some particular things I'd appreciate a second set of eyes on:

  • As @dbarkowsky asked about the other day, the keycloak service does not actually return an id field. It's just name and composite. I'm not sure what is supposed to go in the KeycloakGroupId field of roles then, so I've left it blank where relevant.
  • Please check the implementation of sync users and sync roles to make sure it appears correct.
  • I wasn't sure what field to use for our internal users 'Username', but I'm pretty sure it's supposed to be the keycloak "preferred_username", as this is the only correct username when looking up roles in keycloak. Let me know if this isn't the case.

Copy link
Collaborator

@dbarkowsky dbarkowsky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a few things that I think we need to meet as a group about and discuss what we want out of these services/controllers for standarization.

  1. I think we need a way to determine which HTTP codes to send back when a service fails. At the moment, lots of them will throw a generic 400 it seems. This is the custom error I've been throwing in the other PR to try and deal with that:
export class ErrorWithCode extends Error {
  public code: number;

  constructor(message: string, code?: number) {
    super(message);
    this.code = code;
  }
}

Then this could be caught in the controller and returned with a custom code/message.

  1. What's the easiest way to handle typing with interfaces vs types made from TypeORM? Seems like there are probably going to be conversions, so maybe there's a way to organize these.

Also, to answer some questions from your other comment:

  1. I think the KeycloakId might be useless anyway. We don't seem to actually use it anywhere.
  2. Logic on the sync functions looks good. We'll know for sure after we try running it.
  3. There's a username property that comes back when searching for users in Keycloak. I suspect it's this, because the ones in the database end with @idir too.
{
  "data": [
    {
      "username": "fohe4m5pn8clhkxmlho33sn1r7vr7m67@idir",
      "email": "julius-caesar@email.com",
      "firstName": "Julius",
      "lastName": "Caesar",
      "attributes": {
        "display_name": [
          "Julius Caesar"
        ],
        "idir_user_guid": [
          "fohe4m5pn8clhkxmlho33sn1r7vr7m67"
        ],
        "idir_username": [
          "JULIUSCA"
        ]
      }
    }
  ]
}

@GrahamS-Quartech
Copy link
Contributor Author

1. I think we need a way to determine which HTTP codes to send back when a service fails. At the moment, lots of them will throw a generic 400 it seems. This is the custom error I've been throwing in the other PR to try and deal with that:

Yeah I think this is good, we should probably handle it this way.

Then this could be caught in the controller and returned with a custom code/message.

2. What's the easiest way to handle typing with interfaces vs types made from TypeORM? Seems like there are probably going to be conversions, so maybe there's a way to organize these.

Well, we can probably just use the generated typeorm types in most cases right? Though I guess there may be times we need a different shape to the object.

3. There's a `username` property that comes back when searching for users in Keycloak. I suspect it's this, because the ones in the database end with `@idir` too.

This is the same format as the preferred_username that the keycloak services are returning, so I think that's the right one to use then.

…d with whatever extra columns are necessary. It is also possible to easily join from either side of the relation.
…t found. Fixed some of the schemas again so that users can only have one agency at a time instead of multiple.
Copy link
Collaborator

@dbarkowsky dbarkowsky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there's still a disconnect about where the user roles should be grabbed from. In the original API, they are coming from the database it looks like, not directly from Keycloak. What should our source of truth be? If it's Keycloak, what are the benefits to even storing the role mappings in the database? This affects functions like this:

// How you interpreted it
const getKeycloakRoles = async () => {
  const roles = await KeycloakService.getKeycloakRoles();
  return roles.map((a) => a.name);
};

// How I interpreted it
const getUserRoles = async (username: string) => {
  return await AppDataSource.getRepository(UserRoles).find({
    where: {
      User: {
        Username: username,
      },
    },
    relations: {
      Role: true,
    },
  });
};

express-api/src/controllers/admin/users/usersController.ts Outdated Show resolved Hide resolved
Comment on lines +18 to +39
const decodeJWT = (jwt: string) => {
try {
return JSON.parse(Buffer.from(jwt, 'base64').toString('ascii'));
} catch {
throw new Error('Invalid input in decodeJWT()');
}
};

if (!req.token) return res.status(400).send('No access token');
const [header, payload] = req.token.split('.');
if (!header || !payload) return res.status(400).send('Bad token format.');

const info = {
header: decodeJWT(header),
payload: decodeJWT(payload),
};

if (info) {
return res.status(200).send(info.payload);
} else {
return res.status(400).send('No keycloak user authenticated.');
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might be able to do away with this. If we just want user info, it should be available under the res.user object in the backend and state.userinfo in the frontend.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The user info contained in user doesn't have the same fields as this as far as I can tell, but I agree that this seems like something we could do away with. Not really sure why we would need to expose this.

};

const getRoleById = async (roleId: UUID) => {
return AppDataSource.getRepository(Roles).findOne({
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return AppDataSource.getRepository(Roles).findOne({
return await AppDataSource.getRepository(Roles).findOne({

express-api/src/services/admin/rolesServices.ts Outdated Show resolved Hide resolved
if (!existing) {
throw new ErrorWithCode('Role was not found.', 409);
}
const retRole = AppDataSource.getRepository(Roles).remove(role);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const retRole = AppDataSource.getRepository(Roles).remove(role);
const retRole = await AppDataSource.getRepository(Roles).remove(role);

@Index({ unique: true })
KeycloakUserId: string;

@Column({ name: 'AgencyId', type: 'varchar', length: 6 })
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make the AgencyId column nullable to accommodate the users in PIMS that have no agencies?

@Column({ name: 'AgencyId', type: 'varchar', length: 6, nullable: true })

Copy link
Collaborator

@dbarkowsky dbarkowsky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. Let's get this in so we can keep rolling.

@GrahamS-Quartech GrahamS-Quartech merged commit b55ed13 into main Feb 2, 2024
2 of 3 checks passed
@GrahamS-Quartech GrahamS-Quartech deleted the PIMS-669-UsersRolesServices branch February 2, 2024 00:55
TaylorFries pushed a commit that referenced this pull request Feb 13, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants