Skip to content
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

Add syncrepl (rfc-4533) consumer (persistent search) #447

Merged
merged 5 commits into from Aug 5, 2023

Conversation

t2y
Copy link
Contributor

@t2y t2y commented Jul 19, 2023

After I discussed it with @cpuschma, I changed the design for maintainability.

Design

I added the syncrepl feature as one of the asynchronous search functions. The syncrepl is available on slapd in OpenLDAP project and it provides a persistent search. The syncrepl is proposed by rfc-4533.


(the below description is the original when I created this PR)

(Old) Design

At first, I tried to implement syncrepl feature into the same source. For example, the searchResponse struct has an asynchronous search process, DecodeControl() function has to decode various controls. However, I am concerned that they are challenging to handle syncrepl communications with other generic LDAP communications.

It has a complicated specification because syncrepl is proposed by rfc-4533. I think dividing existent control.go/response.go and related syncrepl code is stable and maintainable (Of course both belong to the same ldap package). Additionally, they won't put a bug on each other.

  • control_syncrepl.go
  • response_syncrepl.go

Data flow

syncrepl has 4 Controls:

  • Sync Request Control is used for client requests
  • Sync State Control is used for server responses
    • request with mode=RefreshAndPersist, then get a response with ApplicationSearchResultEntry (4)
  • Sync Done Control is used for server responses
    • request with mode=RefreshOnly, then get a response with ApplicationSearchResultDone (5)
  • Sync Info Control is used for server responses
    • request with mode=RefreshAndPersist, then get a response with ApplicationIntermediateResponse (25)
Client                                                  Server
  |                                                      |
  |------ Sync Request Control ------------------------> |
  |                                                      |
  | <-------- Sync Info Message (optional) ------------- |
  |                                                      |
  |      (One or more times for each entry)              |
  | <----- Sync State Control (with Entry) ------------- |
  |                                                      |
  | <-------- Sync Info Message (optional) ------------- |
  |                                                      |
  | <----------- Sync Done Control --------------------- |
  |                                                      |

Controls

Sync Request Control as the following.

Control Type: Sync Request ("1.3.6.1.4.1.4203.1.9.1.1") Criticality: true Mode: 3 Cookie: rid=000,csn=20230719002941.016654Z#000000#000#000000 ReloadHint: false

Sync Request with mode=RefreshAndPersist

Got entries and Sync Info Control as the following.

Control Type: Sync Info ("1.3.6.1.4.1.4203.1.9.1.4") Criticality: false Value: 1 RefreshDelete[Cookie: rid=000,csn=20230719074010.612296Z#000000#000#000000 RefreshDone: true]

Control Type: Sync Info ("1.3.6.1.4.1.4203.1.9.1.4") Criticality: false Value: 3 SyncIdSet[Cookie: rid=000,csn=20230719072347.153542Z#000000#000#000000 RefreshDeletes: true SyncUUIDs: [f1688779-d7f3-4c20-9580-24dcb21149a9 1ee6cb16-86c5-41a7-a52a-9168f1f24703 3d11877e-a030-4007-aafe-c94bb0284fdb]]

When a user is added

Got the entry and a Sync State Control such as the following.

Control Type: Sync State ("1.3.6.1.4.1.4203.1.9.1.2") Criticality: false State: 1 EntryUUID: c033cfc4-89cb-4571-9088-6e62be597266 Cookie: rid=000,csn=20230719062607.286081Z#000000#000#000000

When a user is modified

Got the entry and a Sync State Control such as the following.

Control Type: Sync State ("1.3.6.1.4.1.4203.1.9.1.2") Criticality: false State: 2 EntryUUID: 33a88143-1334-4f6b-853c-27239af30c54 Cookie: rid=000,csn=20230719062553.176851Z#000000#000#000000

When a user is deleted

Got the entry and a Sync State Control such as the following.

Control Type: Sync State ("1.3.6.1.4.1.4203.1.9.1.2") Criticality: false State: 3 EntryUUID: 1ee6cb16-86c5-41a7-a52a-9168f1f24703 Cookie: rid=000,csn=20230719071122.501306Z#000000#000#000000

Sync Request with mode=RefreshOnly

Got entries and a Sync Done Control such as the following.

Control Type: Sync Done ("1.3.6.1.4.1.4203.1.9.1.3") Criticality: false Cookie: rid=000,csn=20230719002941.016654Z#000000#000#000000 RefreshDeletes: true

How to test

To enable syncrepl on the server side, add the below configuration into slapd.conf.

overlay syncprov

Then, call Syncrepl() method described in examples_test.go. I confirmed syncrepl feature works with OpenLDAP server 2.4/2.5 in my environment.

I need more test patterns or more considerations since I'm new to LDAP protocol. Could you review it?

References

@t2y t2y force-pushed the add-syncrepl-consumer branch 2 times, most recently from da18de8 to 91f9494 Compare July 19, 2023 13:49
@t2y
Copy link
Contributor Author

t2y commented Jul 19, 2023

#446 will fix DATA RACE with GetLastError(). It is not an error due to this PR.

@t2y
Copy link
Contributor Author

t2y commented Jul 22, 2023

I rebased to include #446 for CI (github actions).

@t2y
Copy link
Contributor Author

t2y commented Jul 30, 2023

@cpuschma @johnweldon I've been waiting for this feature since March 2023 (#422). I have added the syncrepl feature so that the existing process has no side effects. What do you think?

Copy link
Member

@cpuschma cpuschma left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once again, thank you very much for your PR and your constant help improving the project. Overall, thinks are looking good but we need to stay focused to not derivate too much from existing patterns. I'm always open for feedback if you disagree. @go-ldap/committers any comments?

return r.sr.Next()
}

func (r *syncreplResponse) start(ctx context.Context, searchRequest *SearchRequest) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This mostly a copy of the ´startmethod forSearchAsync`. I think we should minize any code duplication, as we already have a copy of basically everythig because of Go module compatibility.

I suggest to remove this duplicate and use a control with SearchAsync instead. This would reduce the LOC dramatically, the amount of work required when thinks need to update and is more in line with the approaches used for regular search or modify requests.

The only think that needs to change is this addition to the start method of SearchAsync, to account for the syncrepl control:

case ApplicationIntermediateResponse:
	decoded, err := DecodeSyncreplControl(packet.Children[1])
	if err != nil {
		werr := fmt.Errorf("failed to decode intermediate response: %w", err)
		r.sr.ch <- &SearchSingleResult{Error: werr}
		return
	}
	result := &SearchSingleResult{}
	result.Controls = append(result.Controls, decoded)
	r.sr.ch <- result

Copy link
Contributor Author

@t2y t2y Jul 31, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest to remove this duplicate and use a control with SearchAsync instead.

I also tried to implement the syncrepl feature into the searchResponse struct with SearchAsync() method. And then, I found we have to change several code places. Because existent search response packets and syncrepl search response packets are a little different. Not only ApplicationIntermediateResponse.

For example, compare handling ApplicationSearchResultEntry of the response packet. The searchResponse doesn't handle controls.

  • searchResponse
				switch packet.Children[1].Tag {
				case ApplicationSearchResultEntry:
					r.ch <- &SearchSingleResult{
						Entry: &Entry{
							DN:         packet.Children[1].Children[0].Value.(string),
							Attributes: unpackAttributes(packet.Children[1].Children[1].Children),
						},
					}

But, the syncreplResponse expects an entry with a control. To implement the syncrepl functions, I thought it might change existing behavior (I'm not sure whether the existing behavior is correct or not). Do you think this is acceptable?

  • syncreplResponse
				case ApplicationSearchResultEntry:
					result := &SearchSingleResult{
						Entry: &Entry{
							DN:         packet.Children[1].Children[0].Value.(string),
							Attributes: unpackAttributes(packet.Children[1].Children[1].Children),
						},
					}
					if len(packet.Children) != 3 {
						r.sr.ch <- result
						continue
					}
					controlPacket := packet.Children[2].Children[0]
					decoded, err := DecodeSyncreplControl(controlPacket)
					if err != nil {
						werr := fmt.Errorf("failed to decode search result entry: %w", err)
						result.Error = werr
						r.sr.ch <- result
						return
					}
					result.Controls = append(result.Controls, decoded)
					r.sr.ch <- result

I think adding the syncrepl feature makes increasing some check condition codes to work them together (both normal search and syncrepl consumer). I can implement them, but I'm not sure whether some side effects affect a normal search or not. This is a trade-off between maintenance cost and compatibility/efficiency.

Copy link
Member

@johnweldon johnweldon Aug 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that reducing unnecessary code duplication is a worthy goal, in this case I'm okay with a distinct syncreplResponse

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cpuschma I integrated the source code for maintainability. I did change the existing process carefully, so it is almost an additional function. There are only three parts to be changed. That is few than I expected. So I think you are right about maintainability.

Could you review it again?

@johnweldon
Copy link
Member

I agree with @cpuschma; thank you for your work on this feature.

My involvement with this project is very limited now, so I'm okay with deferring to the other @go-ldap/committers as far as requested changes.

My overarching values are:

  • Keeping the public surface area of this library as limited as possible (without sacrificing new, useful features)
  • Striving for comprehensive code test coverage

With this in mind, my vote is to approve this MR

@t2y
Copy link
Contributor Author

t2y commented Aug 4, 2023

@johnweldon Thank you for reviewing! I agree with your idea.

And then, I will try to implement this PR without duplicating the code this weekend. I think it's possible, but it might be a little complex. I will carefully integrate my new code to prevent the side effect.

Copy link
Member

@cpuschma cpuschma left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, please see the two notes. After that we can merge the PR

response.go Outdated Show resolved Hide resolved
v3/response.go Outdated Show resolved Hide resolved
@cpuschma cpuschma merged commit 9306bae into go-ldap:master Aug 5, 2023
16 checks passed
@t2y t2y deleted the add-syncrepl-consumer branch August 5, 2023 15:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants