Skip to content

Instantly share code, notes, and snippets.

@josevalim
Created June 28, 2023 09:35
Show Gist options
  • Save josevalim/94bfd65e8eaf892ed700df349838796a to your computer and use it in GitHub Desktop.
Save josevalim/94bfd65e8eaf892ed700df349838796a to your computer and use it in GitHub Desktop.
defmodule Proto.Keys.Key do
defstruct [:key, :protocol_version]
end
defmodule Foo do
use ExUnit.Case, register: false
defp respond_to_latest_org_request(_, _, _, _, _, _ \\ [], _ \\ []) do
:ok
end
test "org approval group enforcement" do
alice = TestClient.create_and_claim_user("alice@somewhere.com", "alice")
%{entity_id: entity_id, admin_initializer: admin_initializer, admin_group_id: admin_group_id} = TestClient.create_org(alice)
params = [
%{user_id: "bob@somewhere.com", display_name: "bob", department: "foo"},
%{user_id: "carlos@somewhere.com", display_name: "carlos", department: "bar"},
%{user_id: "david@somewhere.com", display_name: "david"},
%{user_id: "eliza@somewhere.com", display_name: "eliza"},
%{user_id: "frank@somewhere.com", display_name: "frank"},
%{user_id: "gabby@somewhere.com", display_name: "gabby"}
]
resp = TestClient.post("/users/orgs/#{entity_id}/members", %{users: params}, alice)
assert resp.status == 200, resp.resp_body
assert %{"errors" => []} = Poison.decode!(resp.resp_body)
{secret1, key_version1} = TestClient.receive_claim_email("bob@somewhere.com")
{secret2, key_version2} = TestClient.receive_claim_email("carlos@somewhere.com")
{secret3, key_version3} = TestClient.receive_claim_email("david@somewhere.com")
{secret4, key_version4} = TestClient.receive_claim_email("eliza@somewhere.com")
{secret5, key_version5} = TestClient.receive_claim_email("frank@somewhere.com")
{secret6, key_version6} = TestClient.receive_claim_email("gabby@somewhere.com")
bob = TestClient.claim_user("bob@somewhere.com", secret1, key_version1)
assert %{"entity_id" => ^entity_id, "entity_metadata" => %{"role" => "standard", "department" => "foo"}} = bob.claim_return
carlos = TestClient.claim_user("carlos@somewhere.com", secret2, key_version2)
assert %{"entity_id" => ^entity_id, "entity_metadata" => %{"role" => "standard", "department" => "bar"}} = carlos.claim_return
david = TestClient.claim_user("david@somewhere.com", secret3, key_version3)
assert %{"entity_id" => ^entity_id, "entity_metadata" => %{"role" => "standard", "department" => nil}} = david.claim_return
eliza = TestClient.claim_user("eliza@somewhere.com", secret4, key_version4)
assert %{"entity_id" => ^entity_id, "entity_metadata" => %{"role" => "standard", "department" => nil}} = eliza.claim_return
frank = TestClient.claim_user("frank@somewhere.com", secret5, key_version5)
assert %{"entity_id" => ^entity_id, "entity_metadata" => %{"role" => "standard", "department" => nil}} = frank.claim_return
gabby = TestClient.claim_user("gabby@somewhere.com", secret6, key_version6)
assert %{"entity_id" => ^entity_id, "entity_metadata" => %{"role" => "standard", "department" => nil}} = gabby.claim_return
# create more accounts to test delete pending
params = [
%{user_id: "pending@somewhere.com", display_name: "pending"},
%{user_id: "pending_too@somewhere.com", display_name: "pending_too"}
]
resp = TestClient.post("/users/orgs/#{entity_id}/members", %{users: params}, alice)
assert resp.status == 200, resp.resp_body
assert %{"errors" => []} = Poison.decode!(resp.resp_body)
# create AGs
%{group_id: group_id_1, version: version_1} =
TestClient.create_org_ag(entity_id, alice, "admin_approval_group", [{alice, true}, {carlos, false}, {david, false}], 1)
%{group_id: group_id_2, version: version_2} =
TestClient.create_org_ag(entity_id, alice, "other_approval_group", [{alice, false}, {david, true}, {eliza, true}], 0)
%{group_id: group_id_3, version: version_3} =
TestClient.create_org_ag(entity_id, alice, "export_group", [{alice, false}, {carlos, true}, {david, false}, {eliza, false}], 2)
%{group_id: group_id_4} =
TestClient.create_org_ag(entity_id, alice, "useless_approval_group", [{alice, true}, {bob, false}, {david, false}], 1)
# setting admin_approval_group with non-admin users will be denied
request_params =
%{user_id: alice.user_id,
type: "change_org_approval_group_role",
device_id: alice.device.id,
data: %{group_id: group_id_1, group_role: "admin_approval_group", version: version_1},
timestamp: NaiveDateTime.to_iso8601(NaiveDateTime.utc_now),
expiration: NaiveDateTime.to_iso8601(NaiveDateTime.add(NaiveDateTime.utc_now, 900)),
protocol_version: 1}
encoded_request = Poison.encode!(request_params)
params =
%{
signature: Base.encode64(TestClient.sign_detached(encoded_request, alice.sign_pair.secret)),
request_payload: encoded_request
}
resp = TestClient.post("/users/requests", params, alice)
assert resp.status == 400
# promote bob to admin
admin_changes_hash = Base.encode16(:crypto.hash(:sha256, admin_initializer["payload"]), case: :lower)
payload = Poison.encode!(%{
"timestamp" => NaiveDateTime.to_iso8601(NaiveDateTime.utc_now),
"predecessor" => admin_changes_hash,
"actions" => [%{
"action" => "add",
"user_id" => bob.user_id,
"key_version" => bob.key_version,
"verify_key" => Base.encode64(Proto.Keys.PublicUserKey.encode(%Proto.Keys.Key{
protocol_version: bob.sign_pair.protocol_version,
key: bob.sign_pair.public
}))
}]
})
admin_changes_hash = Base.encode16(:crypto.hash(:sha256, payload), case: :lower)
signature = Base.encode64(TestClient.sign_detached(payload, alice.sign_pair.secret))
admin_changes = %{
user_id: alice.user_id,
key_version: alice.key_version,
signature: signature,
payload: payload
}
group_key_params = %{
sharee_user_id: bob.user_id,
group_id: admin_group_id,
user_key_version: bob.key_version,
group_key_version: 0,
signature: Base.encode64(TestClient.sign_detached("#{bob.user_id},#{bob.key_version},#{admin_group_id},0,#{Base.encode16(:crypto.hash(:sha256, "dummy"), case: :lower)}", alice.sign_pair.secret)),
wrapped_key: Base.encode64("dummy")
}
request_params =
%{user_id: alice.user_id,
type: "change_admin_status",
device_id: alice.device.id,
data: %{user_id: bob.user_id,
role: "admin",
department: "gonna be fired"},
timestamp: NaiveDateTime.to_iso8601(NaiveDateTime.utc_now),
expiration: NaiveDateTime.to_iso8601(NaiveDateTime.add(NaiveDateTime.utc_now, 900)),
protocol_version: 1}
encoded_request = Poison.encode!(request_params)
params =
%{signature: Base.encode64(TestClient.sign_detached(encoded_request, alice.sign_pair.secret)),
request_payload: encoded_request,
admin_changes: admin_changes,
grant_group_key: group_key_params
}
resp = TestClient.post("/users/requests", params, alice)
assert resp.status == 200, resp.resp_body
resp = TestClient.request_users([bob], alice)
assert resp.status == 200, resp.resp_body
assert %{"users" => [
%{"user_id" => "bob@somewhere.com", "claimed" => true, "entity_id" => ^entity_id, "entity_metadata" => %{"role" => "admin", "department" => "gonna be fired"}}
]} = Poison.decode!(resp.resp_body)
# promote carlos to admin
payload = Poison.encode!(%{
"timestamp" => NaiveDateTime.to_iso8601(NaiveDateTime.utc_now),
"predecessor" => admin_changes_hash,
"actions" => [%{
"action" => "add",
"user_id" => carlos.user_id,
"key_version" => carlos.key_version,
"verify_key" => Base.encode64(Proto.Keys.PublicUserKey.encode(%Proto.Keys.Key{
protocol_version: carlos.sign_pair.protocol_version,
key: carlos.sign_pair.public
}))
}]
})
admin_changes_hash = Base.encode16(:crypto.hash(:sha256, payload), case: :lower)
signature = Base.encode64(TestClient.sign_detached(payload, alice.sign_pair.secret))
admin_changes = %{
user_id: alice.user_id,
key_version: alice.key_version,
signature: signature,
payload: payload
}
group_key_params = %{
sharee_user_id: carlos.user_id,
group_id: admin_group_id,
user_key_version: carlos.key_version,
group_key_version: 0,
signature: Base.encode64(TestClient.sign_detached("#{carlos.user_id},#{carlos.key_version},#{admin_group_id},0,#{Base.encode16(:crypto.hash(:sha256, "dummy"), case: :lower)}", alice.sign_pair.secret)),
wrapped_key: Base.encode64("dummy")
}
request_params =
%{user_id: alice.user_id,
type: "change_admin_status",
device_id: alice.device.id,
data: %{user_id: carlos.user_id,
role: "admin",
department: "gonna be demoted"},
timestamp: NaiveDateTime.to_iso8601(NaiveDateTime.utc_now),
expiration: NaiveDateTime.to_iso8601(NaiveDateTime.add(NaiveDateTime.utc_now, 900)),
protocol_version: 1}
encoded_request = Poison.encode!(request_params)
params =
%{signature: Base.encode64(TestClient.sign_detached(encoded_request, alice.sign_pair.secret)),
request_payload: encoded_request,
admin_changes: admin_changes,
grant_group_key: group_key_params
}
resp = TestClient.post("/users/requests", params, alice)
assert resp.status == 200, resp.resp_body
# promote david to admin
payload = Poison.encode!(%{
"timestamp" => NaiveDateTime.to_iso8601(NaiveDateTime.utc_now),
"predecessor" => admin_changes_hash,
"actions" => [%{
"action" => "add",
"user_id" => david.user_id,
"key_version" => david.key_version,
"verify_key" => Base.encode64(Proto.Keys.PublicUserKey.encode(%Proto.Keys.Key{
protocol_version: david.sign_pair.protocol_version,
key: david.sign_pair.public
}))
}]
})
admin_changes_hash = Base.encode16(:crypto.hash(:sha256, payload), case: :lower)
signature = Base.encode64(TestClient.sign_detached(payload, alice.sign_pair.secret))
admin_changes = %{
user_id: alice.user_id,
key_version: alice.key_version,
signature: signature,
payload: payload
}
group_key_params = %{
sharee_user_id: david.user_id,
group_id: admin_group_id,
user_key_version: david.key_version,
group_key_version: 0,
signature: Base.encode64(TestClient.sign_detached("#{david.user_id},#{david.key_version},#{admin_group_id},0,#{Base.encode16(:crypto.hash(:sha256, "dummy"), case: :lower)}", alice.sign_pair.secret)),
wrapped_key: Base.encode64("dummy")
}
request_params =
%{user_id: alice.user_id,
type: "change_admin_status",
device_id: alice.device.id,
data: %{user_id: david.user_id,
role: "admin",
department: "right hand man"},
timestamp: NaiveDateTime.to_iso8601(NaiveDateTime.utc_now),
expiration: NaiveDateTime.to_iso8601(NaiveDateTime.add(NaiveDateTime.utc_now, 900)),
protocol_version: 1}
encoded_request = Poison.encode!(request_params)
params =
%{signature: Base.encode64(TestClient.sign_detached(encoded_request, alice.sign_pair.secret)),
request_payload: encoded_request,
admin_changes: admin_changes,
grant_group_key: group_key_params
}
resp = TestClient.post("/users/requests", params, alice)
assert resp.status == 200, resp.resp_body
# promote gabby to admin
payload = Poison.encode!(%{
"timestamp" => NaiveDateTime.to_iso8601(NaiveDateTime.utc_now),
"predecessor" => admin_changes_hash,
"actions" => [%{
"action" => "add",
"user_id" => gabby.user_id,
"key_version" => gabby.key_version,
"verify_key" => Base.encode64(Proto.Keys.PublicUserKey.encode(%Proto.Keys.Key{
protocol_version: gabby.sign_pair.protocol_version,
key: gabby.sign_pair.public
}))
}]
})
admin_changes_hash = Base.encode16(:crypto.hash(:sha256, payload), case: :lower)
signature = Base.encode64(TestClient.sign_detached(payload, alice.sign_pair.secret))
admin_changes = %{
user_id: alice.user_id,
key_version: alice.key_version,
signature: signature,
payload: payload
}
group_key_params = %{
sharee_user_id: gabby.user_id,
group_id: admin_group_id,
user_key_version: gabby.key_version,
group_key_version: 0,
signature: Base.encode64(TestClient.sign_detached("#{gabby.user_id},#{gabby.key_version},#{admin_group_id},0,#{Base.encode16(:crypto.hash(:sha256, "dummy"), case: :lower)}", alice.sign_pair.secret)),
wrapped_key: Base.encode64("dummy")
}
request_params =
%{user_id: alice.user_id,
type: "change_admin_status",
device_id: alice.device.id,
data: %{user_id: gabby.user_id,
role: "admin",
department: "to delete"},
timestamp: NaiveDateTime.to_iso8601(NaiveDateTime.utc_now),
expiration: NaiveDateTime.to_iso8601(NaiveDateTime.add(NaiveDateTime.utc_now, 900)),
protocol_version: 1}
encoded_request = Poison.encode!(request_params)
params =
%{signature: Base.encode64(TestClient.sign_detached(encoded_request, alice.sign_pair.secret)),
request_payload: encoded_request,
admin_changes: admin_changes,
grant_group_key: group_key_params
}
resp = TestClient.post("/users/requests", params, alice)
assert resp.status == 200, resp.resp_body
# promote eliza to admin
payload = Poison.encode!(%{
"timestamp" => NaiveDateTime.to_iso8601(NaiveDateTime.utc_now),
"predecessor" => admin_changes_hash,
"actions" => [%{
"action" => "add",
"user_id" => eliza.user_id,
"key_version" => eliza.key_version,
"verify_key" => Base.encode64(Proto.Keys.PublicUserKey.encode(%Proto.Keys.Key{
protocol_version: eliza.sign_pair.protocol_version,
key: eliza.sign_pair.public
}))
}]
})
admin_changes_hash = Base.encode16(:crypto.hash(:sha256, payload), case: :lower)
signature = Base.encode64(TestClient.sign_detached(payload, alice.sign_pair.secret))
admin_changes = %{
user_id: alice.user_id,
key_version: alice.key_version,
signature: signature,
payload: payload
}
group_key_params = %{
sharee_user_id: eliza.user_id,
group_id: admin_group_id,
user_key_version: eliza.key_version,
group_key_version: 0,
signature: Base.encode64(TestClient.sign_detached("#{eliza.user_id},#{eliza.key_version},#{admin_group_id},0,#{Base.encode16(:crypto.hash(:sha256, "dummy"), case: :lower)}", alice.sign_pair.secret)),
wrapped_key: Base.encode64("dummy")
}
request_params =
%{user_id: alice.user_id,
type: "change_admin_status",
device_id: alice.device.id,
data: %{user_id: eliza.user_id,
role: "admin",
department: "to delete"},
timestamp: NaiveDateTime.to_iso8601(NaiveDateTime.utc_now),
expiration: NaiveDateTime.to_iso8601(NaiveDateTime.add(NaiveDateTime.utc_now, 900)),
protocol_version: 1}
encoded_request = Poison.encode!(request_params)
params =
%{signature: Base.encode64(TestClient.sign_detached(encoded_request, alice.sign_pair.secret)),
request_payload: encoded_request,
admin_changes: admin_changes,
grant_group_key: group_key_params
}
resp = TestClient.post("/users/requests", params, alice)
assert resp.status == 200, resp.resp_body
# can set AG on member right away
resp = TestClient.get("/users/orgs/#{entity_id}/groups", alice)
assert resp.status == 200, resp.resp_body
decoded_resp = Poison.decode!(resp.resp_body)
assert %{
"name" => "other_approval_group",
"id" => ^group_id_2,
"version" => ^version_2,
"group" => %{
"optionals_required" => 0,
"approvers" => g2_approvers
},
"is_deleted" => false,
"rev_id" => _
} = Enum.find(decoded_resp["groups"], fn x -> x["id"] == group_id_2 end)
TestClient.set_org_ag(alice, gabby, group_id_2, g2_approvers, 0)
# gabby has AG set
resp = TestClient.get("/users/approvers/info?#{URI.encode_query(user_id: gabby.user_id)}", gabby)
assert resp.status == 200, resp.resp_body
assert %{"optional_users" => [%{"user_id" => "alice@somewhere.com", "display_name" => "alice"}],
"required_users" => [_ | _],
"id" => ^group_id_2,
"optionals_required" => 0} = Poison.decode! resp.resp_body
# deleting a user should go through right away
payload = Poison.encode!(%{
"timestamp" => NaiveDateTime.to_iso8601(NaiveDateTime.utc_now),
"predecessor" => admin_changes_hash,
"actions" => [%{
"action" => "remove",
"user_id" => gabby.user_id,
"key_version" => gabby.key_version,
"verify_key" => Base.encode64(Proto.Keys.PublicUserKey.encode(%Proto.Keys.Key{
protocol_version: gabby.sign_pair.protocol_version,
key: gabby.sign_pair.public
}))
}]
})
admin_changes_hash = Base.encode16(:crypto.hash(:sha256, payload), case: :lower)
signature = Base.encode64(TestClient.sign_detached(payload, alice.sign_pair.secret))
admin_changes = %{
user_id: alice.user_id,
key_version: alice.key_version,
signature: signature,
payload: payload
}
request_params =
%{user_id: alice.user_id,
type: "delete_user",
device_id: alice.device.id,
data: %{user_id: gabby.user_id},
timestamp: NaiveDateTime.to_iso8601(NaiveDateTime.utc_now),
expiration: NaiveDateTime.to_iso8601(NaiveDateTime.add(NaiveDateTime.utc_now, 900)),
protocol_version: 1}
encoded_request = Poison.encode!(request_params)
params =
%{signature: Base.encode64(TestClient.sign_detached(encoded_request, alice.sign_pair.secret)),
request_payload: encoded_request,
admin_changes: admin_changes
}
resp = TestClient.post("/users/requests", params, alice)
assert resp.status == 200, resp.resp_body
# gabby no longer exists
resp = TestClient.request_users([gabby], alice)
assert resp.status == 200, resp.resp_body
assert %{"users" => [], "errors" => [e]} = Poison.decode!(resp.resp_body)
assert %{"title" => "missing-entity"} = e
# test deleting a pending user going through right away
# pending user exists
resp = TestClient.post("/users/find", %{spec: [%{user_id: "pending@somewhere.com"}]}, alice)
assert resp.status == 200, resp.resp_body
assert %{"users" => [%{"display_name" => "pending"}], "errors" => []} = Poison.decode!(resp.resp_body)
request_params =
%{user_id: alice.user_id,
type: "delete_user",
device_id: alice.device.id,
data: %{user_id: "pending@somewhere.com"},
timestamp: NaiveDateTime.to_iso8601(NaiveDateTime.utc_now),
expiration: NaiveDateTime.to_iso8601(NaiveDateTime.add(NaiveDateTime.utc_now, 900)),
protocol_version: 1}
encoded_request = Poison.encode!(request_params)
params =
%{signature: Base.encode64(TestClient.sign_detached(encoded_request, alice.sign_pair.secret)),
request_payload: encoded_request
}
resp = TestClient.post("/users/requests", params, alice)
assert resp.status == 200, resp.resp_body
# pending user no longer exists
resp = TestClient.post("/users/find", %{spec: [%{user_id: "pending@somewhere.com"}]}, alice)
assert resp.status == 200, resp.resp_body
assert %{"users" => [], "errors" => [e]} = Poison.decode!(resp.resp_body)
assert %{"title" => "missing-entity"} = e
# setting the admin approval group is via the approval group API
request_params =
%{user_id: alice.user_id,
type: "change_org_approval_group_role",
device_id: alice.device.id,
data: %{group_id: group_id_1, group_role: "admin_approval_group", version: version_1},
timestamp: NaiveDateTime.to_iso8601(NaiveDateTime.utc_now),
expiration: NaiveDateTime.to_iso8601(NaiveDateTime.add(NaiveDateTime.utc_now, 900)),
protocol_version: 1}
encoded_request = Poison.encode!(request_params)
params =
%{
signature: Base.encode64(TestClient.sign_detached(encoded_request, alice.sign_pair.secret)),
request_payload: encoded_request
}
resp = TestClient.post("/users/requests", params, alice)
assert resp.status == 200, resp.resp_body
resp = TestClient.get("/users/orgs/#{entity_id}", alice)
assert resp.status == 200, resp.resp_body
assert %{"roled_approval_groups" => %{"admin_approval_group" => %{"group_id" => ^group_id_1, "version" => ^version_1}}} = Poison.decode!(resp.resp_body)
# cannot delete a group in use as a member's AG
resp = TestClient.delete("/users/orgs/#{entity_id}/groups/#{group_id_1}", alice)
assert resp.status == 403, resp.resp_body
# keep track of amount of requests made - for testing paging of requests
org_requests_count = 0
# test changing the admin approval group
# first, giving an invalid UUID as new admin group id should fail right away
request_params =
%{user_id: alice.user_id,
type: "change_org_approval_group_role",
device_id: alice.device.id,
data: %{group_id: CollectionServer.Types.UUID.generate, group_role: "admin_approval_group", version: version_2},
timestamp: NaiveDateTime.to_iso8601(NaiveDateTime.utc_now),
expiration: NaiveDateTime.to_iso8601(NaiveDateTime.add(NaiveDateTime.utc_now, 900)),
protocol_version: 1}
encoded_request = Poison.encode!(request_params)
params =
%{signature: Base.encode64(TestClient.sign_detached(encoded_request, alice.sign_pair.secret)),
request_payload: encoded_request
}
resp = TestClient.post("/users/requests", params, alice)
assert resp.status == 400, resp.resp_body
# Alice makes request to change admin approval group
request_params =
%{user_id: alice.user_id,
type: "change_org_approval_group_role",
device_id: alice.device.id,
data: %{group_id: group_id_2, group_role: "admin_approval_group", version: version_2},
timestamp: NaiveDateTime.to_iso8601(NaiveDateTime.utc_now),
expiration: NaiveDateTime.to_iso8601(NaiveDateTime.add(NaiveDateTime.utc_now, 900)),
protocol_version: 1}
encoded_request = Poison.encode!(request_params)
params =
%{signature: Base.encode64(TestClient.sign_detached(encoded_request, alice.sign_pair.secret)),
request_payload: encoded_request
}
resp = TestClient.post("/users/requests", params, alice)
assert resp.status == 200, resp.resp_body
assert %{"request" => %{"request_id" => _request_id, "type" => "change_org_approval_group_role"}} = Poison.decode! resp.resp_body
org_requests_count = org_requests_count + 1
# the org admin approval group still the first; no changes made
resp = TestClient.get("/users/orgs/#{entity_id}", alice)
assert resp.status == 200, resp.resp_body
assert %{"roled_approval_groups" => %{"admin_approval_group" => %{"group_id" => ^group_id_1, "version" => ^version_1}}} = Poison.decode!(resp.resp_body)
# Alice fetches her requests
resp = TestClient.get("/users/orgs/#{entity_id}/requests", alice)
assert resp.status == 200, resp.resp_body
assert %{"requests" => [%{"type" => "change_org_approval_group_role", "status" => "pending"}]} = Poison.decode!(resp.resp_body)
# Bob can see the request too, as an admin
resp = TestClient.get("/users/orgs/#{entity_id}/requests", bob)
assert resp.status == 200, resp.resp_body
assert %{"requests" => [%{"type" => "change_org_approval_group_role", "status" => "pending"}]} = Poison.decode!(resp.resp_body)
# alice should see that she has automatically approved her own request
resp = TestClient.get("/users/approvals?#{URI.encode_query(user_id: alice.user_id)}", alice)
assert resp.status == 200, resp.resp_body
assert %{"approvals" => [%{"request_id" => request_id,
"payload" => payload,
"status" => "pending",
"response" => "approved"}]} = Poison.decode! resp.resp_body
# Alice shouldn't be able to change her response or resubmit it.
params = %{requester_user_id: alice.user_id,
signature: Base.encode64(TestClient.sign_detached(payload, alice.sign_pair.secret)),
approve: false}
resp = TestClient.put("/users/orgs/#{entity_id}/requests/#{request_id}", params, alice)
assert resp.status == 409, resp.resp_body
params = %{requester_user_id: alice.user_id,
signature: Base.encode64(TestClient.sign_detached(payload, alice.sign_pair.secret)),
approve: true}
resp = TestClient.put("/users/orgs/#{entity_id}/requests/#{request_id}", params, alice)
assert resp.status == 409, resp.resp_body
# alice can check status of the request
resp = TestClient.get("/users/orgs/#{entity_id}/requests", alice)
assert resp.status == 200, resp.resp_body
assert %{"requests" => [%{"type" => "change_org_approval_group_role", "status" => "pending", "request_id" => request_id}]} = Poison.decode!(resp.resp_body)
resp = TestClient.get("/users/orgs/#{entity_id}/requests/#{request_id}/responses", alice)
assert resp.status == 200, resp.resp_body
assert %{"responses" => r} = Poison.decode!(resp.resp_body)
assert Enum.count(r) == 3
# carlos fetches and denies her request
respond_to_latest_org_request(carlos, entity_id, request_id, alice.user_id, false)
# david fetches and approves her request
respond_to_latest_org_request(david, entity_id, request_id, alice.user_id, true)
# alice should see the new admin approver group
resp = TestClient.get("/users/orgs/#{entity_id}", alice)
assert resp.status == 200, resp.resp_body
assert %{"roled_approval_groups" => %{"admin_approval_group" => %{"group_id" => ^group_id_2, "version" => ^version_2}}} = Poison.decode!(resp.resp_body)
# alice can check status of the request
resp = TestClient.get("/users/orgs/#{entity_id}/requests", alice)
assert resp.status == 200, resp.resp_body
assert %{"requests" => [%{"type" => "change_org_approval_group_role", "status" => "approved"}]} = Poison.decode!(resp.resp_body)
# david should see the request as no longer pending
resp = TestClient.get("/users/approvals?#{URI.encode_query(user_id: david.user_id)}", david)
assert resp.status == 200, resp.resp_body
assert %{"approvals" => [%{"status" => "approved", "response" => "approved"}]} = Poison.decode! resp.resp_body
# keep track of amount of approvals by the new admin group - for testing paging of approvals
approvals_count = 0
# test request setting AG for members
# david has empty AG
resp = TestClient.get("/users/approvers/info?#{URI.encode_query(user_id: david.user_id)}", david)
assert resp.status == 200, resp.resp_body
assert %{"id" => _,
"optional_users" => [],
"required_users" => [],
"optionals_required" => 0} = Poison.decode! resp.resp_body
# alice makes request to set davids's and carlos' AG to group_1
# TODO: currently don't default to admin AG
resp = TestClient.get("/users/orgs/#{entity_id}/groups", alice)
assert resp.status == 200, resp.resp_body
decoded_resp = Poison.decode!(resp.resp_body)
assert %{
"name" => "admin_approval_group",
"id" => ^group_id_1,
"version" => ^version_1,
"group" => %{
"optionals_required" => 1,
"approvers" => g1_approvers
},
"is_deleted" => false,
"rev_id" => _
} = Enum.find(decoded_resp["groups"], fn x -> x["id"] == group_id_1 end)
event_payload = %{
type: :set_approval_group,
timestamp: NaiveDateTime.to_iso8601(NaiveDateTime.utc_now),
expiration: NaiveDateTime.to_iso8601(NaiveDateTime.add(NaiveDateTime.utc_now, 900)),
for_user_id: david.user_id,
data: %{
id: group_id_1,
optionals_required: 1,
approvers: g1_approvers
}
}
david_event_payload = Poison.encode!(event_payload)
david_signature = Base.encode64(TestClient.sign_detached(david_event_payload, alice.sign_pair.secret))
carlos_event_payload = Poison.encode!(%{event_payload | for_user_id: carlos.user_id})
carlos_signature = Base.encode64(TestClient.sign_detached(carlos_event_payload, alice.sign_pair.secret))
request_params =
%{user_id: alice.user_id,
type: "set_member_approval_group",
device_id: alice.device.id,
data: %{current_group_id: nil,
current_group_version: nil,
requester_key_version: alice.key_version,
events: [
%{user_id: david.user_id,
signature: david_signature,
payload: david_event_payload
},
%{user_id: carlos.user_id,
signature: carlos_signature,
payload: carlos_event_payload
}
]},
timestamp: NaiveDateTime.to_iso8601(NaiveDateTime.utc_now),
expiration: NaiveDateTime.to_iso8601(NaiveDateTime.add(NaiveDateTime.utc_now, 900)),
protocol_version: 1}
encoded_request = Poison.encode!(request_params)
params =
%{signature: Base.encode64(TestClient.sign_detached(encoded_request, alice.sign_pair.secret)),
request_payload: encoded_request
}
resp = TestClient.post("/users/requests", params, alice)
assert resp.status == 200, resp.resp_body
# carlos gets empty AG, still
resp = TestClient.get("/users/approvers/info?#{URI.encode_query(user_id: carlos.user_id)}", carlos)
assert resp.status == 200, resp.resp_body
assert %{"id" => _,
"optional_users" => [],
"required_users" => [],
"optionals_required" => 0} = Poison.decode! resp.resp_body
# carlos should now have an event
resp = TestClient.get("/users/events?#{URI.encode_query(user_id: carlos.user_id)}", carlos)
assert resp.status == 200, resp.resp_body
assert %{"last_rev_id" => _, "events" => [%{
"id" => event_id,
"user_id" => "alice@somewhere.com",
"key_version" => n,
"signature" => ^carlos_signature,
"payload" => ^carlos_event_payload,
"handled" => false,
"rev_id" => _
}]} = Poison.decode!(resp.resp_body)
assert n == alice.key_version
assert TestClient.verify_detached(Base.decode64!(carlos_signature), carlos_event_payload, alice.sign_pair.public) == :valid
request = %{
approvers: [
%{
user_id: alice.user_id,
secret: "whatever",
key_version: alice.key_version,
account_version: 0,
required: true,
protocol_version: 2
},
%{
user_id: carlos.user_id,
secret: "something",
key_version: carlos.key_version,
account_version: 0,
required: false,
protocol_version: 2
},
%{
user_id: david.user_id,
secret: "nothing",
key_version: david.key_version,
account_version: 0,
required: false,
protocol_version: 2
}
],
optionals_required: 1
}
resp = TestClient.put("/users/events/#{event_id}", %{user_id: carlos.user_id, request: request}, carlos)
assert resp.status == 200, resp.resp_body
# carlos should have an ag set
resp = TestClient.get("/users/approvers/info?#{URI.encode_query(user_id: carlos.user_id)}", carlos)
assert resp.status == 200, resp.resp_body
assert %{"optional_users" => _,
"required_users" => _,
"id" => ^group_id_1,
"optionals_required" => 1} = Poison.decode! resp.resp_body
# test changing export group
event_payload = Poison.encode!(%{
type: :submit_shards_to_export_group,
timestamp: NaiveDateTime.to_iso8601(NaiveDateTime.utc_now),
expiration: NaiveDateTime.to_iso8601(NaiveDateTime.add(NaiveDateTime.utc_now, 900)),
for_user_id: nil,
data: %{group_id: group_id_3, group_version: version_3}
})
signature = Base.encode64(TestClient.sign_detached(event_payload, alice.sign_pair.secret))
event_params = %{
user_id: nil, # all members of organization
requester_id: alice.user_id,
requester_key_version: alice.key_version,
signature: signature,
payload: event_payload
}
request_params =
%{user_id: alice.user_id,
type: "change_org_approval_group_role",
device_id: alice.device.id,
data: %{group_id: group_id_3, group_role: "export_approval_group", version: version_3, event: event_params},
timestamp: NaiveDateTime.to_iso8601(NaiveDateTime.utc_now),
expiration: NaiveDateTime.to_iso8601(NaiveDateTime.add(NaiveDateTime.utc_now, 900)),
protocol_version: 1}
encoded_request = Poison.encode!(request_params)
params =
%{
signature: Base.encode64(TestClient.sign_detached(encoded_request, alice.sign_pair.secret)),
request_payload: encoded_request
}
resp = TestClient.post("/users/requests", params, alice)
assert resp.status == 200, resp.resp_body
assert %{"request" => %{"request_id" => request_id, "type" => "change_org_approval_group_role"}} = Poison.decode! resp.resp_body
org_requests_count = org_requests_count + 1
approvals_count = approvals_count + 1
resp = TestClient.get("/users/orgs/#{entity_id}", alice)
assert resp.status == 200, resp.resp_body
%{"roled_approval_groups" => ags} = Poison.decode!(resp.resp_body)
assert ags["export_approval_group"] == nil
# since no export AG set, the admin group must allow the change
# david fetches and approves her request
respond_to_latest_org_request(david, entity_id, request_id, alice.user_id, true)
# eliza fetches and approves her request
respond_to_latest_org_request(eliza, entity_id, request_id, alice.user_id, true)
# alice conforms change
resp = TestClient.get("/users/orgs/#{entity_id}", alice)
assert resp.status == 200, resp.resp_body
assert %{"roled_approval_groups" => %{"admin_approval_group" => %{"group_id" => ^group_id_2, "version" => ^version_2},
"export_approval_group" => %{"group_id" => ^group_id_3, "version" => ^version_3}}} = Poison.decode!(resp.resp_body)
# users should've gotten an event
resp = TestClient.get("/users/events?#{URI.encode_query(user_id: carlos.user_id)}", carlos)
assert resp.status == 200
assert Enum.count(Poison.decode!(resp.resp_body)["events"]) == 2
# now changing the export group requires old export group's approval
event_payload = Poison.encode!(%{
type: :submit_shards_to_export_group,
timestamp: NaiveDateTime.to_iso8601(NaiveDateTime.utc_now),
expiration: NaiveDateTime.to_iso8601(NaiveDateTime.add(NaiveDateTime.utc_now, 900)),
for_user_id: nil,
data: %{group_id: group_id_1, group_version: version_1}
})
signature = Base.encode64(TestClient.sign_detached(event_payload, alice.sign_pair.secret))
event_params = %{
user_id: nil, # all members of organization
requester_id: alice.user_id,
requester_key_version: alice.key_version,
signature: signature,
payload: event_payload
}
request_params =
%{user_id: alice.user_id,
type: "change_org_approval_group_role",
device_id: alice.device.id,
data: %{group_id: group_id_1, group_role: "export_approval_group", version: version_1, event: event_params},
timestamp: NaiveDateTime.to_iso8601(NaiveDateTime.utc_now),
expiration: NaiveDateTime.to_iso8601(NaiveDateTime.add(NaiveDateTime.utc_now, 900)),
protocol_version: 1}
encoded_request = Poison.encode!(request_params)
params =
%{
signature: Base.encode64(TestClient.sign_detached(encoded_request, alice.sign_pair.secret)),
request_payload: encoded_request
}
resp = TestClient.post("/users/requests", params, alice)
assert resp.status == 200, resp.resp_body
assert %{"request" => %{"request_id" => _request_id, "type" => "change_org_approval_group_role"}} = Poison.decode! resp.resp_body
org_requests_count = org_requests_count + 1
approvals_count = approvals_count + 1
# alice should have request already approved
resp = TestClient.get("/users/approvals?#{URI.encode_query(user_id: alice.user_id)}", alice)
assert resp.status == 200, resp.resp_body
assert %{"approvals" => [%{"request_id" => request_id,
"payload" => _payload,
"status" => "pending",
"response" => "approved"} | _]} = Poison.decode!(resp.resp_body)
# carlos fetches and approves her request
respond_to_latest_org_request(carlos, entity_id, request_id, alice.user_id, true)
# ensure no change to the group yet
resp = TestClient.get("/users/orgs/#{entity_id}", alice)
assert resp.status == 200, resp.resp_body
assert %{"roled_approval_groups" => %{"admin_approval_group" => %{"group_id" => ^group_id_2, "version" => ^version_2},
"export_approval_group" => %{"group_id" => ^group_id_3, "version" => ^version_3}}} = Poison.decode!(resp.resp_body)
# eliza fetches and approves her request
respond_to_latest_org_request(eliza, entity_id, request_id, alice.user_id, true)
resp = TestClient.get("/users/orgs/#{entity_id}", alice)
assert resp.status == 200, resp.resp_body
assert %{"roled_approval_groups" => %{"admin_approval_group" => %{"group_id" => ^group_id_2, "version" => ^version_2},
"export_approval_group" => %{"group_id" => ^group_id_1, "version" => ^version_1}}} = Poison.decode!(resp.resp_body)
# users should've gotten an event
resp = TestClient.get("/users/events?#{URI.encode_query(user_id: carlos.user_id)}", carlos)
assert resp.status == 200
assert Enum.count(Poison.decode!(resp.resp_body)["events"]) == 3
# alice makes request for carlos to rekey and set AG to group 2
resp = TestClient.get("/users/orgs/#{entity_id}/groups", alice)
assert resp.status == 200, resp.resp_body
decoded_resp = Poison.decode!(resp.resp_body)
assert %{
"name" => "other_approval_group",
"id" => ^group_id_2,
"version" => ^version_2,
"group" => %{
"optionals_required" => 0,
"approvers" => g2_approvers
},
"is_deleted" => false,
"rev_id" => _
} = Enum.find(decoded_resp["groups"], fn x -> x["id"] == group_id_2 end)
carlos_event_payload = Poison.encode!(%{
type: :rekey_and_set_approval_group,
timestamp: NaiveDateTime.to_iso8601(NaiveDateTime.utc_now),
expiration: NaiveDateTime.to_iso8601(NaiveDateTime.add(NaiveDateTime.utc_now, 900)),
for_user_id: carlos.user_id,
data: %{
id: group_id_2,
version: version_2,
optionals_required: 0,
approvers: g2_approvers
}
})
carlos_signature = Base.encode64(TestClient.sign_detached(carlos_event_payload, alice.sign_pair.secret))
# Shouldn't work without providing group info
request_params =
%{user_id: alice.user_id,
type: "member_rekey_and_set_approval_group",
device_id: alice.device.id,
data: %{current_group_id: nil,
current_group_version: nil,
requester_key_version: alice.key_version,
events: [
%{user_id: carlos.user_id,
signature: carlos_signature,
payload: carlos_event_payload
}
]},
timestamp: NaiveDateTime.to_iso8601(NaiveDateTime.utc_now),
expiration: NaiveDateTime.to_iso8601(NaiveDateTime.add(NaiveDateTime.utc_now, 900)),
protocol_version: 1}
encoded_request = Poison.encode!(request_params)
params =
%{signature: Base.encode64(TestClient.sign_detached(encoded_request, alice.sign_pair.secret)),
request_payload: encoded_request
}
resp = TestClient.post("/users/requests", params, alice)
assert resp.status == 400, resp.resp_body
# works when we set the group id and version
request_params =
%{user_id: alice.user_id,
type: "member_rekey_and_set_approval_group",
device_id: alice.device.id,
data: %{
current_group_id: group_id_1,
current_group_version: version_1,
requester_key_version: alice.key_version,
events: [
%{user_id: carlos.user_id,
signature: carlos_signature,
payload: carlos_event_payload
}
]},
timestamp: NaiveDateTime.to_iso8601(NaiveDateTime.utc_now),
expiration: NaiveDateTime.to_iso8601(NaiveDateTime.add(NaiveDateTime.utc_now, 900)),
protocol_version: 1}
encoded_request = Poison.encode!(request_params)
params =
%{signature: Base.encode64(TestClient.sign_detached(encoded_request, alice.sign_pair.secret)),
request_payload: encoded_request
}
resp = TestClient.post("/users/requests", params, alice)
assert resp.status == 200, resp.resp_body
assert %{"request" => %{"request_id" => request_id, "type" => "member_rekey_and_set_approval_group"}} = Poison.decode! resp.resp_body
org_requests_count = org_requests_count + 1
# not approved by the admin AG, so do no raise that count for eliza
# carlos still has old AG
resp = TestClient.get("/users/approvers/info?#{URI.encode_query(user_id: carlos.user_id)}", carlos)
assert resp.status == 200, resp.resp_body
assert %{"id" => ^group_id_1, "optionals_required" => 1} = Poison.decode! resp.resp_body
# carlos doesn't have a new event yet
resp = TestClient.get("/users/events?#{URI.encode_query(user_id: carlos.user_id)}", carlos)
assert resp.status == 200
assert Enum.count(Poison.decode!(resp.resp_body)["events"]) == 3
# alice sees her response as already-approved
resp = TestClient.get("/users/approvals?#{URI.encode_query(user_id: alice.user_id)}", alice)
assert resp.status == 200, resp.resp_body
assert %{"approvals" => [%{"request_id" => ^request_id,
"payload" => _payload,
"status" => "pending",
"response" => "approved"} | _]} = Poison.decode!(resp.resp_body)
# david fetches and approves her request
respond_to_latest_org_request(david, entity_id, request_id, alice.user_id, true)
# carlos should now have an event
resp = TestClient.get("/users/events?#{URI.encode_query(user_id: carlos.user_id)}", carlos)
assert resp.status == 200, resp.resp_body
assert %{"last_rev_id" => _, "events" => [%{
"id" => event_id,
"user_id" => "alice@somewhere.com",
"key_version" => n,
"signature" => ^carlos_signature,
"payload" => ^carlos_event_payload,
"handled" => false,
"rev_id" => _
} | _]} = Poison.decode!(resp.resp_body)
assert n == alice.key_version
assert TestClient.verify_detached(Base.decode64!(carlos_signature), carlos_event_payload, alice.sign_pair.public) == :valid
# as an admin, carlos creates new key, makes admin changes struct, shares group key with new key, and shards key for his new AG and current export group
{sign_pair, _, public_key, public_key_proto} = TestClient.generate_keys(1)
payload = Poison.encode!(%{
"timestamp" => NaiveDateTime.to_iso8601(NaiveDateTime.utc_now),
"predecessor" => admin_changes_hash,
"actions" => [%{
"action" => "add",
"user_id" => carlos.user_id,
"key_version" => 1,
"verify_key" => Base.encode64(Proto.Keys.PublicUserKey.encode(%Proto.Keys.Key{
protocol_version: 1,
key: sign_pair.public
}))
}]
})
admin_changes_hash = Base.encode16(:crypto.hash(:sha256, payload), case: :lower)
signature = Base.encode64(TestClient.sign_detached(payload, carlos.sign_pair.secret))
admin_changes = %{
user_id: carlos.user_id,
key_version: carlos.key_version,
signature: signature,
payload: payload
}
grant_group_key_params = %{
sharee_user_id: carlos.user_id,
group_id: admin_group_id,
user_key_version: carlos.key_version,
group_key_version: 0,
signature: Base.encode64(TestClient.sign_detached("#{carlos.user_id},#{carlos.key_version},#{admin_group_id},0,#{Base.encode16(:crypto.hash(:sha256, "dummy"), case: :lower)}", carlos.sign_pair.secret)),
wrapped_key: Base.encode64("dummy")
}
request = %{
approvers: [
%{
user_id: "alice@somewhere.com",
secret: "whatever",
key_version: alice.key_version,
account_version: 0,
required: false,
protocol_version: 2
},
%{
user_id: "david@somewhere.com",
secret: "anything",
key_version: david.key_version,
account_version: 0,
required: true,
protocol_version: 2
},
%{
user_id: "eliza@somewhere.com",
secret: "something",
key_version: eliza.key_version,
account_version: 0,
required: true,
protocol_version: 2
}
],
optionals_required: 0,
public_key: public_key_proto,
wrapped_last_key: Base.encode64("foo"),
admin_changes: admin_changes,
grant_group_key: grant_group_key_params,
export_group_id: group_id_1,
export_group_version: version_1,
export_approvers: [
%{user_id: alice.user_id,
key_version: 0,
secret: Base.encode64("thing 1"),
wrapped_key_version: 0,
sharder_key_version: 0,
protocol_version: 2
},
%{user_id: carlos.user_id,
key_version: 0,
secret: Base.encode64("do not lose this"),
wrapped_key_version: 0,
sharder_key_version: 0,
protocol_version: 2
},
%{user_id: david.user_id,
key_version: 0,
secret: Base.encode64("real shard for sure"),
wrapped_key_version: 0,
sharder_key_version: 0,
protocol_version: 2
}
]
}
resp = TestClient.put("/users/events/#{event_id}", %{user_id: carlos.user_id, request: request}, carlos)
assert resp.status == 200, resp.resp_body
# carlos should not be able to use old key
resp = TestClient.get("/users/approvers/info?#{URI.encode_query(user_id: carlos.user_id)}", carlos)
assert resp.status == 498, resp.resp_body
# carlos should now be able to use the new key
carlos = %{carlos | sign_pair: sign_pair, public_key: public_key, key_version: 1}
# carlos should have ag set to 2
resp = TestClient.get("/users/approvers/info?#{URI.encode_query(user_id: carlos.user_id)}", carlos)
assert resp.status == 200, resp.resp_body
assert %{"optional_users" => [%{"user_id" => "alice@somewhere.com", "display_name" => "alice"}],
"required_users" => [_ | _],
"id" => ^group_id_2,
"optionals_required" => 0} = Poison.decode! resp.resp_body
# request admin change
resp = TestClient.request_users([carlos], alice)
assert resp.status == 200, resp.resp_body
assert %{"users" => [
%{"user_id" => "carlos@somewhere.com", "claimed" => true, "entity_id" => ^entity_id, "entity_metadata" => %{"role" => "admin", "department" => "gonna be demoted"}}
]} = Poison.decode!(resp.resp_body)
# alice makes request to demote carlos
admin_changes_payload = Poison.encode!(%{
"timestamp" => NaiveDateTime.to_iso8601(NaiveDateTime.utc_now),
"predecessor" => admin_changes_hash,
"actions" => [%{
"action" => "remove",
"user_id" => carlos.user_id,
"key_version" => carlos.key_version,
"verify_key" => Base.encode64(Proto.Keys.PublicUserKey.encode(%Proto.Keys.Key{
protocol_version: 1,
key: carlos.sign_pair.public
}))
}]
})
admin_changes_hash = Base.encode16(:crypto.hash(:sha256, admin_changes_payload), case: :lower)
signature = Base.encode64(TestClient.sign_detached(admin_changes_payload, alice.sign_pair.secret))
admin_changes = %{
user_id: alice.user_id,
key_version: alice.key_version,
signature: signature,
payload: admin_changes_payload
}
request_params =
%{user_id: alice.user_id,
type: "change_admin_status",
device_id: alice.device.id,
data: %{user_id: carlos.user_id,
role: "standard",
department: "intern"},
timestamp: NaiveDateTime.to_iso8601(NaiveDateTime.utc_now),
expiration: NaiveDateTime.to_iso8601(NaiveDateTime.add(NaiveDateTime.utc_now, 900)),
protocol_version: 1}
encoded_request = Poison.encode!(request_params)
params =
%{signature: Base.encode64(TestClient.sign_detached(encoded_request, alice.sign_pair.secret)),
request_payload: encoded_request,
admin_changes: admin_changes
}
resp = TestClient.post("/users/requests", params, alice)
assert resp.status == 200, resp.resp_body
assert %{"request" => %{"request_id" => request_id, "type" => "change_admin_status"}} = Poison.decode! resp.resp_body
org_requests_count = org_requests_count + 1
approvals_count = approvals_count + 1
# carlos still admin
resp = TestClient.request_users([carlos], alice)
assert resp.status == 200, resp.resp_body
assert %{"users" => [
%{"user_id" => "carlos@somewhere.com", "claimed" => true, "entity_id" => ^entity_id, "entity_metadata" => %{"role" => "admin", "department" => "gonna be demoted"}}
]} = Poison.decode!(resp.resp_body)
# david fetches and approves her request
signature = Base.encode64(TestClient.sign_detached(admin_changes_payload, david.sign_pair.secret))
admin_changes = %{
user_id: david.user_id,
key_version: david.key_version,
signature: signature,
payload: admin_changes_payload
}
respond_to_latest_org_request(david, entity_id, request_id, alice.user_id, true, admin_changes)
# eliza fetches and approves her request
signature = Base.encode64(TestClient.sign_detached(admin_changes_payload, eliza.sign_pair.secret))
admin_changes = %{
user_id: eliza.user_id,
key_version: eliza.key_version,
signature: signature,
payload: admin_changes_payload
}
respond_to_latest_org_request(eliza, entity_id, request_id, alice.user_id, true, admin_changes)
# carlos no longer admin
resp = TestClient.request_users([carlos], alice)
assert resp.status == 200, resp.resp_body
assert %{"users" => [
%{"user_id" => "carlos@somewhere.com", "claimed" => true, "entity_id" => ^entity_id, "entity_metadata" => %{"role" => "standard", "department" => "intern"}}
]} = Poison.decode!(resp.resp_body)
# cannot request to demote admin AG member
admin_changes_payload = Poison.encode!(%{
"timestamp" => NaiveDateTime.to_iso8601(NaiveDateTime.utc_now),
"predecessor" => admin_changes_hash,
"actions" => [%{
"action" => "remove",
"user_id" => eliza.user_id,
"key_version" => eliza.key_version,
"verify_key" => Base.encode64(Proto.Keys.PublicUserKey.encode(%Proto.Keys.Key{
protocol_version: 1,
key: eliza.sign_pair.public
}))
}]
})
signature = Base.encode64(TestClient.sign_detached(admin_changes_payload, alice.sign_pair.secret))
admin_changes = %{
user_id: alice.user_id,
key_version: alice.key_version,
signature: signature,
payload: admin_changes_payload
}
request_params =
%{user_id: alice.user_id,
type: "change_admin_status",
device_id: alice.device.id,
data: %{user_id: eliza.user_id,
role: "standard",
department: "intern"},
timestamp: NaiveDateTime.to_iso8601(NaiveDateTime.utc_now),
expiration: NaiveDateTime.to_iso8601(NaiveDateTime.add(NaiveDateTime.utc_now, 900)),
protocol_version: 1}
encoded_request = Poison.encode!(request_params)
params =
%{signature: Base.encode64(TestClient.sign_detached(encoded_request, alice.sign_pair.secret)),
request_payload: encoded_request,
admin_changes: admin_changes
}
resp = TestClient.post("/users/requests", params, alice)
assert resp.status == 400, resp.resp_body
# test deleting an org admin (requires admin changes)
admin_changes_payload = Poison.encode!(%{
"timestamp" => NaiveDateTime.to_iso8601(NaiveDateTime.utc_now),
"predecessor" => admin_changes_hash,
"actions" => [%{
"action" => "remove",
"user_id" => bob.user_id,
"key_version" => bob.key_version,
"verify_key" => Base.encode64(Proto.Keys.PublicUserKey.encode(%Proto.Keys.Key{
protocol_version: 1,
key: bob.sign_pair.public
}))
}]
})
admin_changes_hash = Base.encode16(:crypto.hash(:sha256, admin_changes_payload), case: :lower)
signature = Base.encode64(TestClient.sign_detached(admin_changes_payload, alice.sign_pair.secret))
admin_changes = %{
user_id: alice.user_id,
key_version: alice.key_version,
signature: signature,
payload: admin_changes_payload
}
request_params =
%{user_id: alice.user_id,
type: "delete_user",
device_id: alice.device.id,
data: %{user_id: bob.user_id},
timestamp: NaiveDateTime.to_iso8601(NaiveDateTime.utc_now),
expiration: NaiveDateTime.to_iso8601(NaiveDateTime.add(NaiveDateTime.utc_now, 900)),
protocol_version: 1}
encoded_request = Poison.encode!(request_params)
params =
%{signature: Base.encode64(TestClient.sign_detached(encoded_request, alice.sign_pair.secret)),
request_payload: encoded_request,
admin_changes: admin_changes
}
# cannot make the request since bob is in an AG
resp = TestClient.post("/users/requests", params, alice)
assert resp.status == 400, resp.resp_body
# deleting the AG allows request to go through
resp = TestClient.delete("/users/orgs/#{entity_id}/groups/#{group_id_4}", alice)
assert resp.status == 200, resp.resp_body
resp = TestClient.post("/users/requests", params, alice)
assert resp.status == 200, resp.resp_body
assert %{"request" => %{"request_id" => request_id, "type" => "delete_user"}} = Poison.decode! resp.resp_body
org_requests_count = org_requests_count + 1
approvals_count = approvals_count + 1
# bob still exists
resp = TestClient.request_users([bob], alice)
assert resp.status == 200, resp.resp_body
assert %{"users" => [%{"display_name" => "bob"}], "errors" => []} = Poison.decode!(resp.resp_body)
# david fetches and approves her request
admin_changes = %{
user_id: david.user_id,
key_version: david.key_version,
signature: Base.encode64(TestClient.sign_detached(admin_changes_payload, david.sign_pair.secret)),
payload: admin_changes_payload
}
respond_to_latest_org_request(david, entity_id, request_id, alice.user_id, true, admin_changes)
# eliza fetches and approves her request
# a new AG made before bob got deleted needs to be removed for the deletion to go through.
%{group_id: group_id_5} = TestClient.create_org_ag(entity_id, alice, "random_group", [{alice, false}, {bob, false}, {eliza, false}], 2)
admin_changes = %{
user_id: eliza.user_id,
key_version: eliza.key_version,
signature: Base.encode64(TestClient.sign_detached(admin_changes_payload, eliza.sign_pair.secret)),
payload: admin_changes_payload
}
respond_to_latest_org_request(eliza, entity_id, request_id, alice.user_id, true, admin_changes, 409)
# bob still exists
resp = TestClient.request_users([bob], alice)
assert resp.status == 200, resp.resp_body
assert %{"users" => [%{"display_name" => "bob"}], "errors" => []} = Poison.decode!(resp.resp_body)
# delete the new AG to allow eliza's approval to go through
resp = TestClient.delete("/users/orgs/#{entity_id}/groups/#{group_id_5}", alice)
assert resp.status == 200, resp.resp_body
# now eliza's approval is good
respond_to_latest_org_request(eliza, entity_id, request_id, alice.user_id, true, admin_changes)
# bob no longer exists
resp = TestClient.request_users([bob], alice)
assert resp.status == 200, resp.resp_body
assert %{"users" => [], "errors" => [e]} = Poison.decode!(resp.resp_body)
assert %{"title" => "missing-entity"} = e
# cannot request to delete an admin AG member
admin_changes_payload = Poison.encode!(%{
"timestamp" => NaiveDateTime.to_iso8601(NaiveDateTime.utc_now),
"predecessor" => admin_changes_hash,
"actions" => [%{
"action" => "remove",
"user_id" => david.user_id,
"key_version" => david.key_version,
"verify_key" => Base.encode64(Proto.Keys.PublicUserKey.encode(%Proto.Keys.Key{
protocol_version: 1,
key: david.sign_pair.public
}))
}]
})
signature = Base.encode64(TestClient.sign_detached(admin_changes_payload, alice.sign_pair.secret))
admin_changes = %{
user_id: alice.user_id,
key_version: alice.key_version,
signature: signature,
payload: admin_changes_payload
}
request_params =
%{user_id: alice.user_id,
type: "delete_user",
device_id: alice.device.id,
data: %{user_id: david.user_id},
timestamp: NaiveDateTime.to_iso8601(NaiveDateTime.utc_now),
expiration: NaiveDateTime.to_iso8601(NaiveDateTime.add(NaiveDateTime.utc_now, 900)),
protocol_version: 1}
encoded_request = Poison.encode!(request_params)
params =
%{signature: Base.encode64(TestClient.sign_detached(encoded_request, alice.sign_pair.secret)),
request_payload: encoded_request,
admin_changes: admin_changes
}
resp = TestClient.post("/users/requests", params, alice)
assert resp.status == 400, resp.resp_body
# test deleting a pending user with AG enforcement
request_params =
%{user_id: alice.user_id,
type: "delete_user",
device_id: alice.device.id,
data: %{user_id: "pending_too@somewhere.com"},
timestamp: NaiveDateTime.to_iso8601(NaiveDateTime.utc_now),
expiration: NaiveDateTime.to_iso8601(NaiveDateTime.add(NaiveDateTime.utc_now, 900)),
protocol_version: 1}
encoded_request = Poison.encode!(request_params)
params =
%{signature: Base.encode64(TestClient.sign_detached(encoded_request, alice.sign_pair.secret)),
request_payload: encoded_request,
admin_changes: admin_changes
}
resp = TestClient.post("/users/requests", params, alice)
assert resp.status == 200, resp.resp_body
assert %{"request" => %{"request_id" => request_id, "type" => "delete_user"}} = Poison.decode! resp.resp_body
org_requests_count = org_requests_count + 1
approvals_count = approvals_count + 1
# david and eliza approve
respond_to_latest_org_request(david, entity_id, request_id, alice.user_id, true)
respond_to_latest_org_request(eliza, entity_id, request_id, alice.user_id, true)
# test deleting normal user, without admin changes.
# This request is expected to be the last one and will be deleted below for testing.
request_params =
%{user_id: alice.user_id,
type: "delete_user",
device_id: alice.device.id,
data: %{user_id: frank.user_id},
timestamp: NaiveDateTime.to_iso8601(NaiveDateTime.utc_now),
expiration: NaiveDateTime.to_iso8601(NaiveDateTime.add(NaiveDateTime.utc_now, 900)),
protocol_version: 1}
encoded_request = Poison.encode!(request_params)
params =
%{signature: Base.encode64(TestClient.sign_detached(encoded_request, alice.sign_pair.secret)),
request_payload: encoded_request
}
resp = TestClient.post("/users/requests", params, alice)
assert resp.status == 200, resp.resp_body
assert %{"request" => %{"request_id" => request_id, "type" => "delete_user"}} = Poison.decode! resp.resp_body
org_requests_count = org_requests_count + 1
approvals_count = approvals_count + 1
# frank still exists
resp = TestClient.request_users([frank], alice)
assert resp.status == 200, resp.resp_body
assert %{"users" => [%{"display_name" => "frank"}], "errors" => []} = Poison.decode!(resp.resp_body)
# david and eliza approve her request
respond_to_latest_org_request(david, entity_id, request_id, alice.user_id, true)
respond_to_latest_org_request(eliza, entity_id, request_id, alice.user_id, true)
# frank no longer exists
resp = TestClient.request_users([frank], alice)
assert resp.status == 200, resp.resp_body
assert %{"users" => [], "errors" => [e]} = Poison.decode!(resp.resp_body)
assert %{"title" => "missing-entity"} = e
# test paging requests
resp = TestClient.get("/users/orgs/#{entity_id}/requests", alice)
assert resp.status == 200, resp.resp_body
assert %{"requests" => x, "total_rows" => ^org_requests_count} = Poison.decode!(resp.resp_body)
assert Enum.count(x) == org_requests_count
resp = TestClient.get("/users/orgs/#{entity_id}/requests?#{URI.encode_query(limit: 3)}", alice)
assert resp.status == 200, resp.resp_body
assert %{"requests" => x, "total_rows" => ^org_requests_count} = Poison.decode!(resp.resp_body)
assert Enum.count(x) == 3
resp = TestClient.get("/users/orgs/#{entity_id}/requests?#{URI.encode_query(limit: 5, offset: 0)}", alice)
assert resp.status == 200, resp.resp_body
assert %{"requests" => x, "total_rows" => ^org_requests_count} = Poison.decode!(resp.resp_body)
assert Enum.count(x) == 5
resp = TestClient.get("/users/orgs/#{entity_id}/requests?#{URI.encode_query(limit: 50, offset: 5)}", alice)
assert resp.status == 200, resp.resp_body
assert %{"requests" => x, "total_rows" => ^org_requests_count} = Poison.decode!(resp.resp_body)
assert Enum.count(x) == org_requests_count - 5
# test paging approvals
resp = TestClient.get("/users/approvals?#{URI.encode_query(user_id: eliza.user_id)}", eliza)
assert resp.status == 200, resp.resp_body
assert %{"approvals" => x, "total_rows" => ^approvals_count} = Poison.decode!(resp.resp_body)
assert Enum.count(x) == approvals_count
resp = TestClient.get("/users/approvals?#{URI.encode_query(user_id: eliza.user_id, limit: 3)}", eliza)
assert resp.status == 200, resp.resp_body
assert %{"approvals" => x, "total_rows" => ^approvals_count} = Poison.decode!(resp.resp_body)
assert Enum.count(x) == 3
resp = TestClient.get("/users/approvals?#{URI.encode_query(user_id: eliza.user_id, limit: 5, offset: 0)}", eliza)
assert resp.status == 200, resp.resp_body
assert %{"approvals" => x, "total_rows" => ^approvals_count} = Poison.decode!(resp.resp_body)
assert Enum.count(x) == 5
resp = TestClient.get("/users/approvals?#{URI.encode_query(user_id: eliza.user_id, limit: 4, offset: 4)}", eliza)
assert resp.status == 200, resp.resp_body
assert %{"approvals" => x, "total_rows" => ^approvals_count} = Poison.decode!(resp.resp_body)
assert Enum.count(x) == approvals_count - 4
# test request deletion
# confirm amount of requests retrieved
resp = TestClient.get("/users/orgs/#{entity_id}/requests", alice)
assert resp.status == 200, resp.resp_body
assert %{"requests" => r, "total_rows" => ^org_requests_count} = Poison.decode!(resp.resp_body)
assert Enum.count(r) == org_requests_count
# get info of latest request (deleting org admin)
resp = TestClient.get("/users/orgs/#{entity_id}/requests/#{request_id}/responses", alice)
assert resp.status == 200, resp.resp_body
assert %{"responses" => r} = Poison.decode!(resp.resp_body)
assert Enum.count(r) == 3
# approver should see the request
resp = TestClient.get("/users/approvals?#{URI.encode_query(user_id: eliza.user_id)}", eliza)
assert resp.status == 200, resp.resp_body
assert %{"approvals" => r, "total_rows" => ^approvals_count} = Poison.decode! resp.resp_body
assert Enum.count(r) == approvals_count
# delete the request
resp = TestClient.delete("/users/orgs/#{entity_id}/requests/#{request_id}", alice)
assert resp.status == 200, resp.resp_body
# amount of requests retrieved should decrease by 1
org_requests_count = org_requests_count - 1
resp = TestClient.get("/users/orgs/#{entity_id}/requests", alice)
assert resp.status == 200, resp.resp_body
assert %{"requests" => r, "total_rows" => ^org_requests_count} = Poison.decode!(resp.resp_body)
assert Enum.count(r) == org_requests_count
resp = TestClient.get("/users/orgs/#{entity_id}/requests/#{request_id}/responses", alice)
assert %{"responses" => r} = Poison.decode!(resp.resp_body)
assert Enum.count(r) == 0
# approver shouldn't see the request either
approvals_count = approvals_count - 1
resp = TestClient.get("/users/approvals?#{URI.encode_query(user_id: eliza.user_id)}", eliza)
assert resp.status == 200, resp.resp_body
assert %{"approvals" => r, "total_rows" => ^approvals_count} = Poison.decode! resp.resp_body
assert Enum.count(r) == approvals_count
# filter by status
resp = TestClient.get("/users/orgs/#{entity_id}/requests?#{URI.encode_query(status: "pending")}", alice)
assert resp.status == 200, resp.resp_body
assert %{"requests" => x, "total_rows" => 0} = Poison.decode!(resp.resp_body)
assert Enum.count(x) == 0
resp = TestClient.get("/users/orgs/#{entity_id}/requests?#{URI.encode_query(status: "approved")}", alice)
assert resp.status == 200, resp.resp_body
assert %{"requests" => x, "total_rows" => _} = Poison.decode!(resp.resp_body)
assert Enum.all?(x, &(&1["status"] == "approved"))
resp = TestClient.get("/users/orgs/#{entity_id}/requests?#{URI.encode_query(status: "denied")}", alice)
assert resp.status == 200, resp.resp_body
assert %{"requests" => x, "total_rows" => _} = Poison.decode!(resp.resp_body)
assert Enum.all?(x, &(&1["status"] == "denied"))
# filter by type
resp = TestClient.get("/users/orgs/#{entity_id}/requests?#{URI.encode_query(request_type: "change_admin_status")}", alice)
assert resp.status == 200, resp.resp_body
assert %{"requests" => x, "total_rows" => _} = Poison.decode!(resp.resp_body)
assert Enum.all?(x, &(&1["type"] == "change_admin_status"))
resp = TestClient.get("/users/orgs/#{entity_id}/requests?#{URI.encode_query(request_type: "change_org_approval_group_role")}", alice)
assert resp.status == 200, resp.resp_body
assert %{"requests" => x, "total_rows" => _} = Poison.decode!(resp.resp_body)
assert Enum.all?(x, &(&1["type"] == "change_org_approval_group_role"))
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment