-
Notifications
You must be signed in to change notification settings - Fork 0
Cache update #6
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
base: master
Are you sure you want to change the base?
Cache update #6
Changes from all commits
8695959
191e141
e71e56a
ddd7266
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -146,71 +146,99 @@ func (a *Auth) authHandler(w http.ResponseWriter, r *http.Request) { | |
| clientCertHeader := r.Header.Get("X-Client-Cert") | ||
| if clientCertHeader == "" { | ||
| log.Printf("No X-Client-Cert header found") | ||
| w.WriteHeader(http.StatusForbidden) | ||
| if _, err := w.Write([]byte("Access denied: No client certificate")); err != nil { | ||
| log.Printf("failed to write response body: %v", err) | ||
| } | ||
| a.writeForbidden(w, "Access denied: No client certificate") | ||
| return | ||
| } | ||
|
|
||
| // URL decode the certificate (nginx typically URL-encodes it) | ||
| decodedCert, err := url.QueryUnescape(clientCertHeader) | ||
| if err != nil { | ||
| log.Printf("Failed to URL decode certificate: %v", err) | ||
| w.WriteHeader(http.StatusForbidden) | ||
| if _, err := w.Write([]byte("Access denied: Invalid certificate encoding")); err != nil { | ||
| log.Printf("failed to write response body: %v", err) | ||
| } | ||
| a.writeForbidden(w, "Access denied: Invalid certificate encoding") | ||
| return | ||
| } | ||
|
|
||
| // Parse the certificate | ||
| cert, err := a.parseCertificate(decodedCert) | ||
| if err != nil { | ||
| log.Printf("Failed to parse certificate: %v", err) | ||
| w.WriteHeader(http.StatusForbidden) | ||
| if _, err := w.Write([]byte("Access denied: Invalid certificate")); err != nil { | ||
| log.Printf("failed to write response body: %v", err) | ||
| } | ||
| a.writeForbidden(w, "Access denied: Invalid certificate") | ||
| return | ||
| } | ||
|
|
||
| // Extract Organization Name (O) from certificate | ||
| orgName := a.extractOrganizationName(cert) | ||
| if orgName == "" { | ||
| log.Printf("No organization name found in certificate") | ||
| w.WriteHeader(http.StatusForbidden) | ||
| if _, err := w.Write([]byte("Access denied: No organization in certificate")); err != nil { | ||
| log.Printf("failed to write response body: %v", err) | ||
| } | ||
| return | ||
| } | ||
|
|
||
| // Load the expected public key from Pylon using the organization name as hotkey | ||
| expectedPub, err := a.loadExpectedPublicKey(orgName) | ||
| // Extract and sanitize Organization Name (O) from certificate | ||
| sanitizedOrgName, err := a.extractOrganizationName(cert) | ||
| if err != nil { | ||
| log.Printf("Failed to load expected public key for organization '%s': %v", orgName, err) | ||
| w.WriteHeader(http.StatusForbidden) | ||
| if _, err := w.Write([]byte("Access denied: Organization not authorized")); err != nil { | ||
| log.Printf("failed to write response body: %v", err) | ||
| } | ||
| log.Printf("Failed to extract organization name: %v", err) | ||
| a.writeForbidden(w, "Access denied: Invalid organization in certificate") | ||
| return | ||
| } | ||
|
|
||
| // Validate the certificate against the expected public key | ||
| if err := a.validatePublicKey(cert, expectedPub); err != nil { | ||
| log.Printf("Certificate validation failed for organization '%s': %v", orgName, err) | ||
| w.WriteHeader(http.StatusForbidden) | ||
| if _, err := w.Write([]byte("Access denied: Certificate validation failed")); err != nil { | ||
| log.Printf("failed to write response body: %v", err) | ||
| // Check cache first | ||
| if cachedKey, found := a.cache.Get(sanitizedOrgName); found { | ||
| log.Printf("Cache hit for organization '%s'", sanitizedOrgName) | ||
|
|
||
| // Validate with cached key | ||
| if err := a.validatePublicKey(cert, cachedKey); err != nil { | ||
| // Validation failed with cached key - invalidate cache and retry with fresh data | ||
| log.Printf("Certificate validation failed with cached key for organization '%s', invalidating cache and retrying", sanitizedOrgName) | ||
| a.cache.Invalidate(sanitizedOrgName) | ||
|
|
||
| // Fetch fresh public key from Pylon | ||
| expectedPub, err := a.loadExpectedPublicKey(sanitizedOrgName) | ||
| if err != nil { | ||
| log.Printf("Failed to load expected public key for organization '%s' on retry: %v", sanitizedOrgName, err) | ||
| a.writeForbidden(w, "Access denied: Organization not authorized") | ||
| return | ||
| } | ||
| // Store fresh key in cache | ||
| a.cache.Set(sanitizedOrgName, expectedPub) | ||
|
|
||
| // Validate again with fresh key | ||
| if err := a.validatePublicKey(cert, expectedPub); err != nil { | ||
| log.Printf("Certificate validation failed for organization '%s' even after cache refresh: %v", sanitizedOrgName, err) | ||
| a.writeForbidden(w, "Access denied: Certificate validation failed") | ||
| return | ||
| } | ||
| } | ||
| } else { | ||
| log.Printf("Cache miss for organization '%s'", sanitizedOrgName) | ||
|
|
||
| // Cache miss - load from Pylon | ||
| expectedPub, err := a.loadExpectedPublicKey(sanitizedOrgName) | ||
| if err != nil { | ||
| log.Printf("Failed to load expected public key for organization '%s': %v", sanitizedOrgName, err) | ||
| a.writeForbidden(w, "Access denied: Organization not authorized") | ||
| return | ||
| } | ||
| // Store in cache | ||
| a.cache.Set(sanitizedOrgName, expectedPub) | ||
|
|
||
| // Validate with freshly loaded key | ||
| if err := a.validatePublicKey(cert, expectedPub); err != nil { | ||
| log.Printf("Certificate validation failed for organization '%s': %v", sanitizedOrgName, err) | ||
| a.writeForbidden(w, "Access denied: Certificate validation failed") | ||
| return | ||
| } | ||
| return | ||
| } | ||
|
|
||
| // Certificate is valid | ||
| log.Printf("Certificate validation successful for organization '%s'", orgName) | ||
| log.Printf("Certificate validation successful for organization '%s'", sanitizedOrgName) | ||
| a.writeOK(w, "Access granted") | ||
| } | ||
|
|
||
| // writeForbidden writes a 403 Forbidden response with the given message | ||
| func (a *Auth) writeForbidden(w http.ResponseWriter, message string) { | ||
| w.WriteHeader(http.StatusForbidden) | ||
| if _, err := w.Write([]byte(message)); err != nil { | ||
| log.Printf("failed to write response body: %v", err) | ||
| } | ||
| } | ||
|
|
||
| // writeOK writes a 200 OK response with the given message | ||
| func (a *Auth) writeOK(w http.ResponseWriter, message string) { | ||
| w.WriteHeader(http.StatusOK) | ||
| if _, err := w.Write([]byte("Access granted")); err != nil { | ||
| if _, err := w.Write([]byte(message)); err != nil { | ||
| log.Printf("failed to write response body: %v", err) | ||
| } | ||
| } | ||
|
|
@@ -235,12 +263,14 @@ func (a *Auth) parseCertificate(certPEM string) (*x509.Certificate, error) { | |
| return cert, nil | ||
| } | ||
|
|
||
| // extractOrganizationName extracts the Organization Name (O) from the certificate | ||
| func (a *Auth) extractOrganizationName(cert *x509.Certificate) string { | ||
| if len(cert.Subject.Organization) > 0 { | ||
| return cert.Subject.Organization[0] | ||
| // extractOrganizationName extracts and sanitizes the Organization Name (O) from the certificate | ||
| func (a *Auth) extractOrganizationName(cert *x509.Certificate) (string, error) { | ||
| if len(cert.Subject.Organization) == 0 { | ||
| return "", fmt.Errorf("no organization name found in certificate") | ||
| } | ||
| return "" | ||
|
|
||
| orgName := cert.Subject.Organization[0] | ||
| return a.sanitizeOrgName(orgName) | ||
| } | ||
|
|
||
| // sanitizeOrgName sanitizes the organization name to prevent path traversal attacks | ||
|
|
@@ -273,24 +303,10 @@ func (a *Auth) sanitizeOrgName(orgName string) (string, error) { | |
| } | ||
|
|
||
| // loadExpectedPublicKey fetches expected ed25519 public key for given organization (hotkey) from Pylon | ||
| // Uses cache to avoid repeated requests for the same hotkey | ||
| // Note: orgName should already be sanitized by the caller | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we could enforce this instead of adding a comment. (Yes, I'm a type system junkie. No, I'm not well versed in golang, so please bear with me...) Let the orgName be of type SanitizedOrgName, and the only producer of said type be WDYT?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMHO I don't want to create another abstraction and custom type for something that is just a string and is purely internal in However, to address your concern and still satisfy procedural go code, I can just move sanitization to the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm eager to learn. Please tell me, what in
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not, other than it is really redundant IMHO. I was more referring to creating another struct with a new |
||
| func (a *Auth) loadExpectedPublicKey(orgName string) (ed25519.PublicKey, error) { | ||
| // Sanitize the organization name to prevent path traversal attacks | ||
| sanitizedOrgName, err := a.sanitizeOrgName(orgName) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("invalid organization name: %v", err) | ||
| } | ||
|
|
||
| // Check cache first | ||
| if cachedKey, found := a.cache.Get(sanitizedOrgName); found { | ||
| log.Printf("Cache hit for organization '%s'", sanitizedOrgName) | ||
| return cachedKey, nil | ||
| } | ||
|
|
||
| log.Printf("Cache miss for organization '%s'", sanitizedOrgName) | ||
|
|
||
| // Cache miss - fetch from Pylon | ||
| resp, err := a.pylonClient.GetCertificate(sanitizedOrgName) | ||
| // Fetch from Pylon | ||
| resp, err := a.pylonClient.GetCertificate(orgName) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
@@ -307,9 +323,6 @@ func (a *Auth) loadExpectedPublicKey(orgName string) (ed25519.PublicKey, error) | |
| } | ||
| publicKey := ed25519.PublicKey(decoded) | ||
|
|
||
| // Store in cache | ||
| a.cache.Set(sanitizedOrgName, publicKey) | ||
|
|
||
| return publicKey, nil | ||
| } | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I find this code difficult to follow.
I think this would be better:
notice how much shorter the code is, and how there's almost no nested ifs. Also the context doesn't jump around - in the original code when the cached key is correctly validated you first need to jump 20 lines from
if err := a.validatePublicKey(cert, cachedKey); err != nilto the end of the if, then ~25 lines again down to see what happens when you leave the bigifYou loose some context in the log lines (but not in the logs in their entirety), but get much simpler code. I take this tradeoff 10 out of 10 times.
Btw, there is tight coupling of auth logic with response writing, that manifests by
were the code written like
and the caller would use the writer to actually write the decision you could test each aspect separately, and the code wouldn't need much changes if there was another writer used in the future. Now, the logic of deciding whether a cert is good relies on
http.ResponseWriter. There is no need for that.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I kindly disagree.
Current code is much more readable:
if, (b) cache miss inelse. So quick look at the code and you immediately know what is going on. I don't mind have some code duplication, if I can have better logs (maybe it is devops inside me)I am happy to discuss it in more details ;)
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is one of the reasons why the code is needlessly complex.
There isn't one happy path. There are multiple:
They were made convoluted so that they end up together in one line.
So let's consider the complexity of happy paths. I want to check if we don't OK in some case that we shouldn't. That's a reasonable ask, isn't it?
Since I read the code from top to bottom, I don't know you made the decision to return OK as the very last thing in the function.
And so to achieve my goal I need to go through the whole 50 lines of code back and forth and check every set of possible conditions to see what is happening there.
So it goes like this:
read lines 178-181. I need to push "cache was hit" to the context.
then I read 182-184. I need to push "public key not validated" to the context, to make it "cache was hit, public key not validated"
then I read 188-202. I push and pop from the context because there are ifs.
I need to remember that if a set of 3 conditions evaluates to some specific values the access will be forbidden.
In 203/204 I need to scan back to 178 to check what the closing bracket are about. I could pop from the context stack, but by that time I read 20 lines of code, made 4 operations on the context, and have to remember 2 specific cases in which 3 ifs evaluate to specific values, and that then the access is forbidden. That's a lot, so I don't want to make a mistake. I scan back, refresh my memory on the context.
So far I scanned ~50 lines of code already.
in 204 there's an else. I scan down, because for the time being I don't care for happens in the other branch. I could go there, but then the amount of information that I need to remember grows even larger.
So I scan to 226 to learn that now the access is granted.
This means that the if's I read earlier must exhaust all the cases where the access should be forbidden. Does it? I need to scan back to 183-202 part and find the answer to a different question than before:
"do, under the conditions from line 178 and 182 conditions from 189 and 198 exhaust all the possibilities where we should deny access".
I've scanned the same code multiple times and I haven't even touched the else block.
Let's compare that to my proposal:
it has a flat structure, each part is independent. There is no reason to scan back in any place.
The first part:
has ~10 lines and simple semantics:
if there's a correct key in cache - allow access; if not the cache will not have a key.
After reading that part and deeming that semantics is satisfied you can forget about it's contents entirely. The ONLY thing you need to remember is that "if the control progressed after the block then there is no key in the cache"; all scenarios with the key in the cache have been dealt with (correctly).
So far we dealt with "cache hit" scenario. There were ~10 lines and 2 ifs in total.
The second part is also dead simple - load the key to the cache. If it cannot be loaded forbid access:
Again, once you are satisfied this semantics is achieved you can forget about the details of that part. With 1
ifyou reach a point in code where "there is a fresh public key in the cache". All other scenarios have been dealt with correctly, so no need to concern yourself with that.And so there's the final part:
Again, it's dead simple and you can tell the "fresh key in the cache" is handled correctly.
There were 4
ifsin total, the max nesting was 2. You never had to have more than ~10 lines of code in the context + 1 line uncoditional state description. Regardless of whether you were looking for happy paths or negative paths you had to do the same amount of work or the order of function length.In your solution there are 6
ifs, with max nesting 3. It is necessary to keep much more in the context (say, ~20 lines + scanning over lines that are not interesting), keeping an intricate state descriptions likethe cache was hit, the key was invalid, it was unable to load key or it was possible to load the key but they key was not valid.I encourage you to measure code complexity of the proposed solutions using existing tools...
This forces you have a state that is a cross product of different conditions, making the code more complex and more difficult to test, as the conditions are independent (e.g. the key is valid or not regardless of if it was loaded from cache or subtensor).
You can have equally good logs in my solution, but since it's linear you might need to read more than 1 line of log. This is a very good tradeoff, because simpler code means less bugs means less log reading in the first place. Reading 2 lines instead of 1 is near zero cost once you actually start reading the logs.
It is needlessly larger than it needs to be by every metric I know of.
That said, I'm open to any new perspective.
this is completely irrelevant.
FWIW, you handled errors by writing to a http response rather than by returning values, which introduces unnecessary coupling to http, but OK, let's keep the changes to minimum and let's hope there won't be any other use for the auth mechanism and so we won't need to refactor that in the future.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really don't want to delve into this discussion. Clearly, we have different opinion. Just 2 things I want to share.
So please close this PR or like described in the handbook:
let's merge it if you don't see any bugs and you can change it in any way you want after that, signing the commits with your name ;)
No hard feelings from my side. I love heat discussions about coding :) Hopefully you feel the same way,