Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions __tests__/admin-bind-keycloak-account.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Integration test: an admin reads a wallet and binds a keycloak account to a
// legacy wallet that has none (#1241).
require('dotenv').config();
const request = require('supertest');
const { expect } = require('chai');
const uuid = require('uuid');
const server = require('../server/app');
const seed = require('./seed');

// The JWT mock reads "<keycloak id>#<roles>" from the bearer token.
const adminToken = `${uuid.v4()}#wallet-admin`;

describe('Admin binds a keycloak account to a wallet', () => {
beforeEach(async () => {
await seed.clear();
await seed.seed();
});

it('reads a wallet with its keycloak account id', async () => {
const res = await request(server)
.get(`/admin/wallets/${seed.wallet.id}`)
.set('Authorization', `Bearer ${adminToken}`)
.expect(200);

expect(res.body).property('id').eq(seed.wallet.id);
expect(res.body)
.property('keycloak_account_id')
.eq(seed.wallet.keycloak_account_id);
expect(res.body).to.not.have.property('password');
expect(res.body).to.not.have.property('salt');
});

it('binds an account and reports it on the next read', async () => {
const keycloakAccountId = uuid.v4();

const res = await request(server)
.post(`/admin/wallets/${seed.walletB.id}/keycloak-account`)
.set('Authorization', `Bearer ${adminToken}`)
.send({ keycloak_account_id: keycloakAccountId })
.expect(200);

expect(res.body).property('keycloak_account_id').eq(keycloakAccountId);

const after = await request(server)
.get(`/admin/wallets/${seed.walletB.id}`)
.set('Authorization', `Bearer ${adminToken}`)
.expect(200);

expect(after.body).property('keycloak_account_id').eq(keycloakAccountId);
});

it('rebinds a wallet that already has an account', async () => {
const keycloakAccountId = uuid.v4();

const res = await request(server)
.post(`/admin/wallets/${seed.wallet.id}/keycloak-account`)
.set('Authorization', `Bearer ${adminToken}`)
.send({ keycloak_account_id: keycloakAccountId })
.expect(200);

expect(res.body).property('keycloak_account_id').eq(keycloakAccountId);
});

it('lets two wallets share one account', async () => {
const keycloakAccountId = uuid.v4();

await request(server)
.post(`/admin/wallets/${seed.wallet.id}/keycloak-account`)
.set('Authorization', `Bearer ${adminToken}`)
.send({ keycloak_account_id: keycloakAccountId })
.expect(200);

await request(server)
.post(`/admin/wallets/${seed.walletB.id}/keycloak-account`)
.set('Authorization', `Bearer ${adminToken}`)
.send({ keycloak_account_id: keycloakAccountId })
.expect(200);
});

it('refuses a body without a uuid', async () => {
await request(server)
.post(`/admin/wallets/${seed.wallet.id}/keycloak-account`)
.set('Authorization', `Bearer ${adminToken}`)
.send({ keycloak_account_id: 'not-a-uuid' })
.expect(422);
});

it('404s for a wallet that does not exist', async () => {
await request(server)
.post(`/admin/wallets/${uuid.v4()}/keycloak-account`)
.set('Authorization', `Bearer ${adminToken}`)
.send({ keycloak_account_id: uuid.v4() })
.expect(404);
});

it('refuses a signed-in user without the role', async () => {
const res = await request(server)
.post(`/admin/wallets/${seed.wallet.id}/keycloak-account`)
.set('Authorization', `Bearer ${seed.wallet.keycloak_account_id}`)
.send({ keycloak_account_id: uuid.v4() })
.expect(403);

expect(res.body.message).match(/wallet-admin role required/);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
'use strict';

var dbm;
var type;
var seed;
var fs = require('fs');
var path = require('path');
var Promise;

/**
* We receive the dbmigrate dependency from dbmigrate initially.
* This enables us to not have to rely on NODE_PATH.
*/
exports.setup = function (options, seedLink) {
dbm = options.dbmigrate;
type = dbm.dataType;
seed = seedLink;
Promise = options.Promise;
};

exports.up = function (db) {
var filePath = path.join(
__dirname,
'sqls',
'20261006000000-AddKeycloakAccountBoundEventType-up.sql',
);
return new Promise(function (resolve, reject) {
fs.readFile(filePath, { encoding: 'utf-8' }, function (err, data) {
if (err) return reject(err);
console.log('received data: ' + data);

resolve(data);
});
}).then(function (data) {
return db.runSql(data);
});
};

exports.down = function (db) {
var filePath = path.join(
__dirname,
'sqls',
'20261006000000-AddKeycloakAccountBoundEventType-down.sql',
);
return new Promise(function (resolve, reject) {
fs.readFile(filePath, { encoding: 'utf-8' }, function (err, data) {
if (err) return reject(err);
console.log('received data: ' + data);

resolve(data);
});
}).then(function (data) {
return db.runSql(data);
});
};

exports._meta = {
version: 1,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
-- Postgres cannot drop a value from an enum type, so this is irreversible.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TYPE wallet_event_type ADD VALUE IF NOT EXISTS 'keycloak_account_bound';
Comment thread
dadiorchen marked this conversation as resolved.
30 changes: 30 additions & 0 deletions server/handlers/walletHandler/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const {
walletGetQuerySchema,
walletGetAdminQuerySchema,
walletIdParamSchema,
walletKeycloakAccountPostSchema,
walletGetTrustRelationshipsSchema,
walletPostSchema,
walletPatchSchema,
Expand Down Expand Up @@ -315,13 +316,42 @@ const walletGetAdmin = async (req, res) => {
res.status(200).json({ total: count, query: validatedQuery, wallets });
};

const walletSingleGetAdmin = async (req, res) => {
const { wallet_id } = await walletIdParamSchema.validateAsync(req.params, {
abortEarly: false,
});

const wallet = await new WalletService().getWalletAdmin(wallet_id);

res.status(200).json(wallet);
};

const walletKeycloakAccountPost = async (req, res) => {
const { wallet_id } = await walletIdParamSchema.validateAsync(req.params, {
abortEarly: false,
});
const { keycloak_account_id } =
await walletKeycloakAccountPostSchema.validateAsync(req.body, {
abortEarly: false,
});

const wallet = await new WalletService().bindKeycloakAccount(
wallet_id,
keycloak_account_id,
);

res.status(200).json(wallet);
};

module.exports = {
walletPost,
walletPatch,
walletGetTrustRelationships,
walletGet,
walletGetAdmin,
walletSingleGet,
walletSingleGetAdmin,
walletKeycloakAccountPost,
walletBatchCreate,
walletBatchTransfer,
};
5 changes: 5 additions & 0 deletions server/handlers/walletHandler/schemas.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ const walletIdParamSchema = Joi.object({
wallet_id: Joi.string().uuid().required(),
});

const walletKeycloakAccountPostSchema = Joi.object({
keycloak_account_id: Joi.string().uuid().required(),
});

const walletGetTrustRelationshipsSchema = Joi.object({
state: Joi.string().valid(
...Object.values(TrustRelationshipEnums.ENTITY_TRUST_STATE_TYPE),
Expand Down Expand Up @@ -113,6 +117,7 @@ module.exports = {
walletGetQuerySchema,
walletGetAdminQuerySchema,
walletIdParamSchema,
walletKeycloakAccountPostSchema,
walletGetTrustRelationshipsSchema,
walletPostSchema,
walletPatchSchema,
Expand Down
11 changes: 10 additions & 1 deletion server/routes/adminRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,20 @@ const router = express.Router();
const routerWrapper = express.Router();

const { handlerWrapper, verifyRoleHandler } = require('../utils/utils');
const { walletGetAdmin } = require('../handlers/walletHandler');
const {
walletGetAdmin,
walletSingleGetAdmin,
walletKeycloakAccountPost,
} = require('../handlers/walletHandler');

const WALLET_ADMIN_ROLE = 'wallet-admin';

router.get('/wallets', handlerWrapper(walletGetAdmin));
router.get('/wallets/:wallet_id', handlerWrapper(walletSingleGetAdmin));
router.post(
'/wallets/:wallet_id/keycloak-account',
handlerWrapper(walletKeycloakAccountPost),
);

// Mounted apart from /wallets because these endpoints answer about every
// wallet, so they gate on a keycloak role rather than on owning a wallet.
Expand Down
29 changes: 29 additions & 0 deletions server/services/WalletService.js
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,35 @@ class WalletService {
return this._wallet.getAllWalletsAdmin(options);
}

async getWalletAdmin(walletId) {
const wallet = await this._wallet.getById(walletId);
delete wallet.password;
delete wallet.salt;
return wallet;
}

async bindKeycloakAccount(walletId, keycloakAccountId) {
const wallet = await this.getWalletAdmin(walletId);

const updated = await this._wallet.updateWallet({
id: walletId,
keycloak_account_id: keycloakAccountId,
});

await this._event.logEvent({
wallet_id: walletId,
type: EventEnums.WALLET.keycloak_account_bound,
payload: {
previous_keycloak_account_id: wallet.keycloak_account_id,
keycloak_account_id: keycloakAccountId,
},
});

delete updated.password;
delete updated.salt;
return updated;
}

async batchCreateWallet(
sender_wallet,
token_transfer_amount_default,
Expand Down
1 change: 1 addition & 0 deletions server/utils/event-enum.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ EventEnums.TRUST = {

EventEnums.WALLET = {
wallet_created: 'wallet_created',
keycloak_account_bound: 'keycloak_account_bound',
};

module.exports = EventEnums;
Loading