bcgov/common-forms-toolkit

View on GitHub
app/src/components/keycloakAdminService.js

Summary

Maintainability
D
2 days
Test Coverage
// const { Issuer } = require('openid-client');
const KeycloakAdminClient = require('keycloak-admin').default;
const log = require('npmlog');
const Problem = require('api-problem');

const errorToProblem = require('./errorToProblem');

// This is DOSing SSO
// const refreshToken = (svc) => setInterval(async () => {
//   // just in case we didn't go through the initialization phase.
//   await svc.initialize(false);
//   try {
//     const refreshToken = svc._tokenSet.refresh_token;
//     svc._tokenSet = await svc._client.refresh(refreshToken);
//     svc._kcAdminClient.setAccessToken(svc._tokenSet.access_token);
//   } catch (e) {
//     log.error('KeycloakAdminService.refreshToken', `Error refreshing token. Re-initializing/authorizing admin client. ${e.message}`);
//     await svc.initialize(true);
//   }
// }, 58 * 1000); // 58 seconds

const trimUserData = (data, nullDataValue = []) => {
  if (!data) return nullDataValue;

  const trim = u => {
    // only return id, username, firstName, lastName, email
    return (({ id, username, firstName, lastName, email }) => ({ id, username, firstName, lastName, email }))(u);
  };
  if (Array.isArray(data)) {
    return data.map(u => trim(u));
  }
  return trim(data);
};

const trimClientData = (data, nullDataValue = []) => {
  if (!data) return nullDataValue;

  const trim = u => {
    return (({ id, clientId, name, description }) => ({ id, clientId, name, description }))(u);
  };
  if (Array.isArray(data)) {
    return data.map(u => trim(u));
  }
  return trim(data);
};

const SERVICE = 'Keycloak';

class KeycloakAdminService {
  constructor({ baseUrl, realm, clientId, clientSecret }) {
    log.verbose('KeycloakAdminService', `Constructed with ${baseUrl}, ${realm}, ${clientId}, clientSecret`);
    if (!baseUrl || !realm || !clientId || !clientSecret) {
      log.error('KeycloakAdminService', 'Invalid configuration.');
      throw new Error('KeycloakAdminService is not configured. Check configuration.');
    }

    this._baseUrl = baseUrl;
    this._realm = realm;
    this._clientId = clientId;
    this._clientSecret = clientSecret;
    this._issuerUrl = `${this._baseUrl}/realms/${this._realm}`;
    this._grantType = 'client_credentials';

    this._initialized = false;
    this._keycloakIssuer = null;
    this._client = null;
    this._tokenSet = null;

    this._kcAdminClient = new KeycloakAdminClient({
      baseUrl: this._baseUrl,
      realmName: this._realm
    });
    this.initialize(true);
  }

  // Get a token to auth the admin lib
  async _auth() {
    try {
      await this._kcAdminClient.auth({
        grantType: this._grantType,
        clientId: this._clientId,
        clientSecret: this._clientSecret
      });

    } catch (err) {
      log.error('KeycloakAdminService._auth', `Error during authorization of service credentials: ${err.message}`);
      throw (err);
    }
  }

  async initialize(force = false) {
    if (force === true) this._initialized = false;
    if (!this._initialized) {
      // try to do a straight initialization of the client with configured credentials.
      // service should be good to go right away.
      try {
        await this._auth();
      } catch (err) {
        log.error('KeycloakAdminService.initialize', `Error initializing with service credentials: ${err.message}`);
        return false;
      }
      //
      // now, get set up so we can keep refreshing the token for realm admin service...
      //
      // try {
      //   this._keycloakIssuer = await Issuer.discover(this._issuerUrl);
      // } catch (err) {
      //   log.error('KeycloakAdminService.initialize', `Error during discovery of issuer: ${err.message}`);
      //   return false;
      // }

      // try {
      //   this._client = new this._keycloakIssuer.Client({
      //     client_id: this._clientId,
      //     client_secret: this._clientSecret
      //   });
      // } catch (err) {
      //   log.error('KeycloakAdminService.initialize', `Error creating client: ${err.message}`);
      //   return false;
      // }

      // try {
      //   this._tokenSet = await this._client.grant({
      //     grant_type: this._grantType,
      //     client_id: this._clientId,
      //     client_secret: this._clientSecret
      //   });
      // } catch (err) {
      //   log.error('KeycloakAdminService.initialize', `Error getting tokens: ${err.message}`);
      //   return false;
      // }
      this._initialized = true;
    }
    return this._initialized;
  }

  _problem(status, title, detail) {
    throw new Problem(status, title, { detail: detail });
  }

  _notFoundProblem(type, id) {
    throw new Problem(404, 'Not found', { detail: `Could not find ${type} with id ${id}.` });
  }

  async _findClient(id) {
    const result = await this._kcAdminClient.clients.findOne({ id: id });
    if (!result) {
      this._notFoundProblem('client', id);
    }
    return result;
  }

  async _findRole(id) {
    const result = await this._kcAdminClient.roles.findOneById({ id: id });
    if (!result) {
      this._notFoundProblem('role', id);
    }
    return result;
  }

  async _findUser(id) {
    const result = await this._kcAdminClient.users.findOne({ id: id });
    if (!result) {
      this._notFoundProblem('user', id);
    }
    return result;
  }

  async findClients(name, includeRoles = false) {
    try {
      // TEMP: refresh loop was DOSing SSO
      await this._auth();

      let result = await this._kcAdminClient.clients.find();
      let clients = trimClientData(result);
      if (name) {
        clients = clients.filter(x => x.clientId === name);
      }
      if (includeRoles) {
        for (const c of clients) {
          const roles = await this._kcAdminClient.clients.listRoles({ id: c.id });
          c.roles = roles;
        }
      }
      return clients;
    } catch (err) {
      errorToProblem(SERVICE, err);
    }
  }

  async getClient(id, includeRoles = false, includeUsers = false, includeUserRoles = false) {
    try {
      // TEMP: refresh loop was DOSing SSO
      await this._auth();

      const result = await this._findClient(id);
      const client = trimClientData(result);
      client.roles = [];
      if (includeRoles) {
        client.roles = await this._kcAdminClient.clients.listRoles({ id: id });
        for (const r of client.roles) {
          r.users = [];
          if (includeUsers) {
            const users = await this._kcAdminClient.clients.findUsersWithRole({ id: id, roleName: r.name });
            r.users = trimUserData(users);
            for (const ur of r.users) {
              ur.roles = [];
              if (includeUserRoles) {
                ur.roles = await this._kcAdminClient.users.listClientRoleMappings({ id: ur.id, clientUniqueId: id });
              }
            }
          }
        }
      }
      return client;
    } catch (err) {
      errorToProblem(SERVICE, err);
    }
  }

  async findUsers(search) {
    try {
      // TEMP: refresh loop was DOSing SSO
      await this._auth();

      const result = await this._kcAdminClient.users.find({ search: search });
      return trimUserData(result);
    } catch (err) {
      errorToProblem(SERVICE, err);
    }
  }

  async getUser(id) {
    try {
      // TEMP: refresh loop was DOSing SSO
      await this._auth();

      const result = await this._findUser(id);
      return trimUserData(result);
    } catch (err) {
      errorToProblem(SERVICE, err);
    }
  }

  async getClientUser(clientId, userId, includeRoles = false) {
    try {
      // TEMP: refresh loop was DOSing SSO
      await this._auth();

      const result = await this._findUser(userId);
      const user = trimUserData(result);
      if (includeRoles) {
        user.roles = await this._kcAdminClient.users.listClientRoleMappings({ id: userId, clientUniqueId: clientId });
      }
      return user;
    } catch (err) {
      errorToProblem(SERVICE, err);
    }
  }

  async addClientRoleMappings(clientId, users, roles) {
    try {
      // TEMP: refresh loop was DOSing SSO
      await this._auth();

      const roleList = roles.map(x => (({ id, name }) => ({ id, name }))(x));
      for (const u of users) {
        await this._kcAdminClient.users.addClientRoleMappings({ id: u.id, clientUniqueId: clientId, roles: roleList });
      }
    } catch (err) {
      errorToProblem(SERVICE, err);
    }
  }

  async removeClientRoleMappings(clientId, users, roles) {
    try {
      // TEMP: refresh loop was DOSing SSO
      await this._auth();

      const roleList = roles.map(x => (({ id, name }) => ({ id, name }))(x));
      for (const u of users) {
        await this._kcAdminClient.users.delClientRoleMappings({ id: u.id, clientUniqueId: clientId, roles: roleList });
      }
    } catch (err) {
      errorToProblem(SERVICE, err);
    }
  }

  async getClientRoleByName(clientId, name) {
    try {
      // TEMP: refresh loop was DOSing SSO
      await this._auth();

      const role = await this._kcAdminClient.clients.findRole({ id: clientId, roleName: name });
      if (!role) {
        this._notFoundProblem('role', name);
      }
      return role;
    } catch (err) {
      errorToProblem(SERVICE, err);
    }
  }

}

module.exports = KeycloakAdminService;