Skip to content

Commit

Permalink
Support user management in create-daml-app (digital-asset#12089)
Browse files Browse the repository at this point in the history
fixes digital-asset#11998

changelog_begin
changelog_end
  • Loading branch information
cocreature authored Jan 7, 2022
1 parent 1d258a1 commit 2783b7b
Show file tree
Hide file tree
Showing 8 changed files with 120 additions and 40 deletions.
4 changes: 4 additions & 0 deletions compatibility/bazel_tools/create-daml-app/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,10 @@ main :: IO ()
main = withTempDir $ \npmCache -> do
setEnv "npm_config_cache" npmCache True
setEnv "TASTY_NUM_THREADS" "1" True
-- We disable user management since older ledgers do not support it.
-- We might eventually want to enable it when running against > 1.18 but for now
-- this also acts as a nice test for running without user management.
setEnv "REACT_APP_SUPPORTS_USERMANAGEMENT" "false" True
let options =
[ Option @DamlOption Proxy
, Option @DamlLedgerOption Proxy
Expand Down
3 changes: 3 additions & 0 deletions daml-assistant/integration-tests/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ da_haskell_library(
"directory",
"extra",
"filepath",
"process",
"tagged",
"tasty",
"tasty-hunit",
Expand All @@ -242,10 +243,12 @@ da_haskell_test(
args = [
"$(location @nodejs//:npm_bin)",
"$(location @nodejs//:node_bin)",
"$(location @grpcurl_nix//:bin)",
"--project-name",
"not-create-daml-app",
],
data = [
"@grpcurl_nix//:bin",
"@nodejs//:node_bin",
"@nodejs//:npm_bin",
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,16 @@ main :: IO ()
main = withTempDir $ \npmCache -> do
setEnv "npm_config_cache" npmCache True
limitJvmMemory defaultJvmMemoryLimits
npm : node : args <- getArgs
npm : node : grpcurl : args <- getArgs
javaPath <- locateRunfiles "local_jdk/bin"
oldPath <- getSearchPath
npmPath <- takeDirectory <$> locateRunfiles (mainWorkspace </> npm)
-- we need node in scope for the post install step of babel
nodePath <- takeDirectory <$> locateRunfiles (mainWorkspace </> node)
grpcurlPath <- takeDirectory <$> locateRunfiles (mainWorkspace </> grpcurl)
let ingredients = defaultIngredients ++ [includingOptions [Option @ProjectName Proxy]]
withArgs args (withEnv
[ ("PATH", Just $ intercalate [searchPathSeparator] (javaPath : npmPath : nodePath : oldPath))
[ ("PATH", Just $ intercalate [searchPathSeparator] (javaPath : npmPath : nodePath : grpcurlPath : oldPath))
, ("TASTY_NUM_THREADS", Just "1")
] $ defaultMainWithIngredients ingredients tests)

Expand Down
81 changes: 52 additions & 29 deletions templates/create-daml-app-test-resources/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

// Keep in sync with compatibility/bazel_tools/create-daml-app/index.test.ts

import { ChildProcess, spawn, SpawnOptions } from 'child_process';
import { ChildProcess, spawn, spawnSync, SpawnOptions } from 'child_process';
import { promises as fs } from 'fs';
import puppeteer, { Browser, Page } from 'puppeteer';
import waitOn from 'wait-on';
Expand All @@ -26,17 +26,40 @@ let uiProc: ChildProcess | undefined = undefined;
let browser: Browser | undefined = undefined;

// Function to generate unique party names for us.
// This should be replaced by the party management service once that is exposed
// in the HTTP JSON API.
let nextPartyId = 1;
function getParty(): string {
const party = `P${nextPartyId}`;
const getParty = async () : [string, string] => {
// TODO For now we use grpcurl to allocate parties and users.
// Once the JSON API exposes party & user management we can switch to that.
const grpcurlPartyArgs = [
"-plaintext",
"localhost:6865",
"com.daml.ledger.api.v1.admin.PartyManagementService/AllocateParty",
];
const allocResult = spawnSync('grpcurl', grpcurlPartyArgs, {"encoding": "utf8"});
const parsedResult = JSON.parse(allocResult.stdout);
const user = `u${nextPartyId}`;
const party = parsedResult.partyDetails.party;
const createUser = {
"user": {
"id": user,
"primary_party": party,
},
"rights": [{"can_act_as": {"party": party}}]
};
const grpcurlUserArgs = [
"-plaintext",
"-d",
JSON.stringify(createUser),
"localhost:6865",
"com.daml.ledger.api.v1.admin.UserManagementService/CreateUser",
];
const result = spawnSync('grpcurl', grpcurlUserArgs, {"encoding": "utf8"});
nextPartyId++;
return party;
}
return [user, party];
};

test('Party names are unique', async () => {
const parties = new Set(Array(10).fill({}).map(() => getParty()));
const parties = new Set(await Promise.all(Array(10).fill({}).map(() => getParty())));
expect(parties.size).toEqual(10);
});

Expand Down Expand Up @@ -126,13 +149,13 @@ afterAll(async () => {
});

test('create and look up user using ledger library', async () => {
const party = getParty();
const token = authConfig.makeToken(party);
const [user, party] = await getParty();
const token = authConfig.makeToken(user);
const ledger = new Ledger({token});
const users0 = await ledger.query(User.User);
expect(users0).toEqual([]);
const user = {username: party, following: []};
const userContract1 = await ledger.create(User.User, user);
const userPayload = {username: party, following: []};
const userContract1 = await ledger.create(User.User, userPayload);
const userContract2 = await ledger.fetchByKey(User.User, party);
expect(userContract1).toEqual(userContract2);
const users = await ledger.query(User.User);
Expand Down Expand Up @@ -202,27 +225,27 @@ const follow = async (page: Page, userToFollow: string) => {

// LOGIN_TEST_BEGIN
test('log in as a new user, log out and log back in', async () => {
const partyName = getParty();
const [user, party] = await getParty();

// Log in as a new user.
const page = await newUiPage();
await login(page, partyName);
await login(page, user);

// Check that the ledger contains the new User contract.
const token = authConfig.makeToken(partyName);
const token = authConfig.makeToken(user);
const ledger = new Ledger({token});
const users = await ledger.query(User.User);
expect(users).toHaveLength(1);
expect(users[0].payload.username).toEqual(partyName);
expect(users[0].payload.username).toEqual(party);

// Log out and in again as the same user.
await logout(page);
await login(page, partyName);
await login(page, user);

// Check we have the same one user.
const usersFinal = await ledger.query(User.User);
expect(usersFinal).toHaveLength(1);
expect(usersFinal[0].payload.username).toEqual(partyName);
expect(usersFinal[0].payload.username).toEqual(party);

await page.close();
}, 40_000);
Expand All @@ -236,13 +259,13 @@ test('log in as a new user, log out and log back in', async () => {
// These are all successful cases.

test('log in as three different users and start following each other', async () => {
const party1 = getParty();
const party2 = getParty();
const party3 = getParty();
const [user1, party1] = await getParty();
const [user2, party2] = await getParty();
const [user3, party3] = await getParty();

// Log in as Party 1.
const page1 = await newUiPage();
await login(page1, party1);
await login(page1, user1);

// Party 1 should initially follow no one.
const noFollowing1 = await page1.$$('.test-select-following');
Expand All @@ -266,7 +289,7 @@ test('log in as three different users and start following each other', async ()

// Log in as Party 2.
const page2 = await newUiPage();
await login(page2, party2);
await login(page2, user2);

// Party 2 should initially follow no one.
const noFollowing2 = await page2.$$('.test-select-following');
Expand Down Expand Up @@ -305,7 +328,7 @@ test('log in as three different users and start following each other', async ()

// Log in as Party 3.
const page3 = await newUiPage();
await login(page3, party3);
await login(page3, user3);

// Party 3 should follow no one.
const noFollowing3 = await page3.$$('.test-select-following');
Expand All @@ -324,13 +347,13 @@ test('log in as three different users and start following each other', async ()
}, 60_000);

test('error when following self', async () => {
const party = getParty();
const [user, party] = await getParty();
const page = await newUiPage();

const dismissError = jest.fn(dialog => dialog.dismiss());
page.on('dialog', dismissError);

await login(page, party);
await login(page, user);
await follow(page, party);

expect(dismissError).toHaveBeenCalled();
Expand All @@ -339,14 +362,14 @@ test('error when following self', async () => {
});

test('error when adding a user that you are already following', async () => {
const party1 = getParty();
const party2 = getParty();
const [user1, party1] = await getParty();
const [user2, party2] = await getParty();
const page = await newUiPage();

const dismissError = jest.fn(dialog => dialog.dismiss());
page.on('dialog', dismissError);

await login(page, party1);
await login(page, user1);
// First attempt should succeed
await follow(page, party2);
// Second attempt should result in an error
Expand Down
2 changes: 2 additions & 0 deletions templates/create-daml-app/daml.yaml.template
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@ codegen:
js:
output-directory: ui/daml.js
npm-scope: daml.js
# Remove this line when running against a ledger without user management.
init-script: Setup:setup
10 changes: 10 additions & 0 deletions templates/create-daml-app/daml/Setup.daml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module Setup where

import DA.Foldable
import Daml.Script

setup : Script ()
setup = do
forA_ [("Alice", "alice"), ("Bob", "bob"), ("Charlie", "charlie")] $ \(displayName, userName) -> do
p <- allocatePartyWithHint displayName (PartyIdHint ("p" <> userName))
createUser (User userName (Some p)) [CanActAs p]
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,10 @@ const LoginScreen: React.FC<Props> = ({onLogin}) => {
const [username, setUsername] = React.useState('');
const handleLogin = async (event: React.FormEvent) => {
event.preventDefault();
await login({party: username,
const token = auth.makeToken(username);
const ledger = new Ledger({token: token});
const primaryParty = await auth.userManagement.primaryParty(username, ledger);
await login({party: primaryParty,
token: auth.makeToken(username)});
}
return wrap(<>
Expand Down
50 changes: 42 additions & 8 deletions templates/create-daml-app/ui/src/config.ts.template
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,16 @@

import { encode } from 'jwt-simple';
import { isRunningOnHub } from '@daml/hub-react';
import Ledger from '@daml/ledger';

export type UserManagement = {
tokenPayload: (loginName: string, ledgerId: string) => Object,
primaryParty: (loginName: string, ledger: Ledger) => Promise<string>,
};

export type Insecure = {
provider: "none",
userManagement: UserManagement,
makeToken: (party: string) => string,
};

Expand All @@ -21,6 +28,38 @@ export type Auth0 = {

export type Authentication = Insecure | DamlHub | Auth0;

// This needs to be used for ledgers in SDK < 2.0.0 and VMBC <= 1.6
export const noUserManagement: UserManagement = {
tokenPayload: (loginName: string, ledgerId: string) =>
({
"https://daml.com/ledger-api": {
"ledgerId": ledgerId,
"applicationId": '__PROJECT_NAME__',
"actAs": [loginName]
}
}),
primaryParty: async (loginName: string, ledger: Ledger) => loginName,
};

// Used on SDK >= 2.0.0 with the exception of VMBC
export const withUserManagement: UserManagement = {
tokenPayload: (loginName: string, ledgerId: string) =>
({
sub: loginName,
}),
primaryParty: async (loginName, ledger: Ledger) => {
const user = await ledger.getUser();
// TODO https://github.com/digital-asset/daml/issues/12275
// Improve error handling
return user.primaryParty!;
}
};

export const userManagement: UserManagement =
// We default to assuming that user management is enabled so we interpret everything that
// isn’t explicitly "false" as supporting user management.
process.env.REACT_APP_SUPPORTS_USERMANAGEMENT?.toLowerCase() !== "false" ? withUserManagement : noUserManagement;

export const authConfig: Authentication = (() => {
if (isRunningOnHub()) {
const auth: DamlHub = {
Expand All @@ -42,14 +81,9 @@ export const authConfig: Authentication = (() => {
const ledgerId: string = process.env.REACT_APP_LEDGER_ID ?? "__PROJECT_NAME__-sandbox"
const auth: Insecure = {
provider: "none",
makeToken: (party) => {
const payload = {
"https://daml.com/ledger-api": {
"ledgerId": ledgerId,
"applicationId": '__PROJECT_NAME__',
"actAs": [party]
}
}
userManagement: userManagement,
makeToken: (loginName) => {
const payload = userManagement.tokenPayload(loginName, ledgerId);
return encode(payload, "secret", "HS256");
}
};
Expand Down

0 comments on commit 2783b7b

Please sign in to comment.