-
-
Notifications
You must be signed in to change notification settings - Fork 0
Extracted permission object #2
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
Merged
Merged
Changes from all commits
Commits
Show all changes
40 commits
Select commit
Hold shift + click to select a range
77546c5
first permissions
martinheidegger 1f57d6b
making sure that the first member can only add itself
martinheidegger a3504fd
preventing requests from unknown members
martinheidegger f6975a4
first request needs to be add request
martinheidegger 1cdf5d7
first operation can not be a response
martinheidegger bada966
first member can add second member directly
martinheidegger 837bbe1
with more than two members we can not simply add members
martinheidegger 68af094
added request states
martinheidegger 23ba722
adding missing member amount check
martinheidegger ab0b61d
refactor: simplifying logic
martinheidegger aad8194
memo timestamps
martinheidegger 4bb8005
other members may have older timestamps
martinheidegger 40381dc
permissions keeps internal clock
martinheidegger 83b37bc
response for unknown request
martinheidegger a2b18d4
new has method on States
martinheidegger f072494
different error for already finished requests
martinheidegger a9d6a07
allowing only one request to be active
martinheidegger 5c1d27e
fixing bug where re-setting of a state would cause incosistencies
martinheidegger 0e483b3
allowing members to cancel request
martinheidegger 9394a3f
member can not cancel request by other member
martinheidegger 4a21336
denying a request
martinheidegger d8f480e
typo
martinheidegger 8ecd36c
refactor: extracting handleDeny and handleCancel
martinheidegger 275e17c
better error name
martinheidegger 2a78ada
cant accept own request
martinheidegger a22eef9
adding test of accepting request
martinheidegger abc7108
basic acceptance of requests
martinheidegger 66b83aa
adding test to check that multiple members needed to add a request
martinheidegger 650f077
making follow up requests active
martinheidegger e79c3f6
can not remove a non-member
martinheidegger b9ec11c
allowing to remove members
martinheidegger d95a22a
Preventing readding of members
martinheidegger 4111265
Preventing to add already added members.
martinheidegger 5cd330c
Making sure that the same request id is not used twice
martinheidegger 14f0501
Making sure that one signature less is required for remove requests
martinheidegger d5554e0
also returning that request
martinheidegger 2f7f7a1
Allowing for all members to be removed
martinheidegger 68f1efd
States: replacing object access with interface access.
martinheidegger 7e6cb71
cleaned state
martinheidegger 8a83225
started working on versioned state
martinheidegger File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,217 @@ | ||
import { FeedItem, isRequest, isResponse, Request, Response } from './member' | ||
import { States } from './States' | ||
import HLC, { Timestamp } from '@consento/hlc' | ||
|
||
export type MemberState = 'added' | 'removed' | ||
export type RequestState = 'finished' | 'denied' | 'active' | 'pending' | 'conflicted' | 'cancelled' | ||
export type MemberId = string | ||
export type RequestId = string | ||
|
||
function pushToMapped <K, V> (map: Map<K, V[]>, key: K, value: V): number { | ||
const list = map.get(key) | ||
if (list === undefined) { | ||
map.set(key, [value]) | ||
return 1 | ||
} | ||
return list.push(value) | ||
} | ||
|
||
export class Permissions { | ||
readonly members = new States<MemberState>() | ||
readonly requests = new States<RequestState>() | ||
readonly clock = new HLC() | ||
readonly signatures = new Map<RequestId, Set<MemberId>>() | ||
|
||
private readonly memberTime = new Map<MemberId, Timestamp>() | ||
private readonly openRequests = new Map<RequestId, Request>() | ||
private readonly openRequestsByMember = new Map<MemberId, Request[]>() | ||
|
||
get isLocked (): boolean { | ||
if (this.members.byState('removed').size === 0) { | ||
// We havn't removed members yet, so the system is still active: case before the first member. | ||
return false | ||
} | ||
// If all members are removed, no new members can be possibly added; The system is locked. | ||
return this.members.byState('added').size === 0 | ||
} | ||
|
||
add <Input extends FeedItem> (item: Input): Input { | ||
const members = this.members.byState('added') | ||
if (members.size === 0) { | ||
if (!isRequest(item)) { | ||
throw new Error('First feed-item needs to be a request.') | ||
} | ||
if (item.who !== item.from) { | ||
throw new Error('The first member can only add itself.') | ||
} | ||
if (item.operation !== 'add') { | ||
throw new Error('First request needs to be an add request.') | ||
} | ||
if (this.isLocked) { | ||
throw new Error('All members were removed.') | ||
} | ||
} else { | ||
if (!members.has(item.from)) { | ||
throw new Error('unknown member') | ||
} | ||
} | ||
const lastTime = this.memberTime.get(item.from) | ||
if (lastTime !== undefined && lastTime.compare(item.timestamp) >= 0) { | ||
throw new Error(`Order error: The last item from "${item.from}" is newer than this request.`) | ||
} | ||
this.memberTime.set(item.from, item.timestamp) | ||
this.clock.update(item.timestamp) | ||
if (isRequest(item)) { | ||
this.handleRequest(item) | ||
return item | ||
} else if (isResponse(item)) { | ||
this.handleResponse(item) | ||
return item | ||
} | ||
throw new Error('todo') | ||
} | ||
|
||
private handleResponse (response: Response): void { | ||
const state = this.requests.get(response.id) | ||
if (state === undefined) { | ||
throw new Error(`Response for unknown request ${response.id}`) | ||
} | ||
if (state === 'finished') { | ||
throw new Error(`Received response to the already-finished request "${response.id}".`) | ||
} | ||
const openRequest = this.openRequests.get(response.id) | ||
if (response.response === 'cancel') { | ||
return this.handleCancel(response, openRequest) | ||
} | ||
if (response.response === 'deny') { | ||
return this.handleDeny(response, openRequest) | ||
} | ||
if (response.response === 'accept') { | ||
return this.handleAccept(response, openRequest) | ||
} | ||
throw new Error('todo') | ||
} | ||
|
||
private handleAccept (response: Response, openRequest?: Request): void { | ||
if (openRequest !== undefined) { | ||
if (openRequest.from === response.from) { | ||
throw new Error('Cant accept own request.') | ||
} | ||
const signatures = this.addSignature(response) | ||
if (signatures >= this.getRequiredSignatures(openRequest)) { | ||
this.finishRequest(openRequest) | ||
} | ||
} | ||
// TODO: should we thrown an error if the request is not active | ||
} | ||
|
||
private getRequiredSignatures (request: Request): number { | ||
const amountMembers = this.members.byState('added').size | ||
// The signature of the member that created the request is not necessary | ||
const neededSignatures = amountMembers - 1 | ||
if ( | ||
// Remove operations are okay with having one less | ||
request.operation === 'remove' && | ||
// Two members can form a majority, to remove one of two members | ||
// unilaterally is impossible | ||
amountMembers > 2 | ||
) { | ||
return neededSignatures - 1 | ||
} | ||
return neededSignatures | ||
} | ||
|
||
private addSignature (response: Response): number { | ||
const signatures = this.signatures.get(response.id) | ||
if (signatures === undefined) { | ||
this.signatures.set(response.id, new Set(response.from)) | ||
return 1 | ||
} | ||
if (signatures.has(response.from)) { | ||
throw new Error(`${response.from} tried to sign the same request twice`) | ||
} | ||
signatures.add(response.from) | ||
return signatures.size | ||
} | ||
|
||
private finishRequest (request: Request): void { | ||
if (request.operation === 'merge') { | ||
throw new Error('todo') | ||
} | ||
if (request.operation === 'add') { | ||
this.members.set(request.who, 'added') | ||
} else { | ||
this.members.set(request.who, 'removed') | ||
} | ||
this.requests.set(request.id, 'finished') | ||
this.openRequests.delete(request.id) | ||
this.signatures.delete(request.id) | ||
const list = this.openRequestsByMember.get(request.from) | ||
if (list === undefined) { | ||
throw new Error('This may never occur') | ||
} | ||
if (list.shift() !== request) { | ||
throw new Error('This may also never occur') | ||
} | ||
const entry = list[0] | ||
if (entry === undefined) { | ||
this.openRequestsByMember.delete(request.from) | ||
} else { | ||
this.requests.set(entry.id, 'active') | ||
} | ||
} | ||
|
||
private handleCancel (response: Response, openRequest?: Request): void { | ||
if (openRequest !== undefined) { | ||
if (openRequest.from !== response.from) { | ||
throw new Error(`Member ${response.from} can not cancel the request ${response.id} by ${openRequest.from}.`) | ||
} | ||
this.requests.set(response.id, 'cancelled') | ||
this.openRequests.delete(response.id) | ||
this.openRequestsByMember.delete(response.id) | ||
} | ||
// TODO: Should we throw an error if the request is not active? | ||
} | ||
|
||
private handleDeny (response: Response, openRequest?: Request): void { | ||
if (openRequest !== undefined) { | ||
if (openRequest.from === response.from) { | ||
throw new Error(`Member ${response.from} can not deny their own request ${response.id}. Maybe they meant to cancel?`) | ||
} | ||
this.requests.set(response.id, 'denied') | ||
this.openRequests.delete(response.id) | ||
this.openRequestsByMember.delete(response.id) | ||
} | ||
// TODO: Should we throw an error if the request is not active? | ||
} | ||
|
||
private handleRequest (request: Request): void { | ||
if (request.operation === 'merge') { | ||
RangerMauve marked this conversation as resolved.
Show resolved
Hide resolved
|
||
throw new Error('todo') | ||
} | ||
if (this.requests.has(request.id)) { | ||
throw new Error(`Request ID=${request.id} has already been used.`) | ||
} | ||
const memberState = this.members.get(request.who) | ||
if (request.operation === 'remove') { | ||
if (memberState === undefined) { | ||
throw new Error(`Cant remove ${request.who} because it is not a member.`) | ||
} | ||
} else { | ||
if (memberState === 'removed') { | ||
throw new Error(`Cant add previously removed member ${request.who}`) | ||
RangerMauve marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
if (memberState === 'added') { | ||
throw new Error(`Cant add ${request.who} as it is already added`) | ||
} | ||
} | ||
this.openRequests.set(request.id, request) | ||
this.requests.set( | ||
request.id, | ||
pushToMapped(this.openRequestsByMember, request.from, request) === 1 ? 'active' : 'pending' | ||
) | ||
if (this.requests.get(request.id) === 'active' && this.getRequiredSignatures(request) <= 0) { | ||
this.finishRequest(request) | ||
} | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.