-
Notifications
You must be signed in to change notification settings - Fork 1.3k
[NDINT-290] Versa SLA Metrics w/ Session Auth #37300
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 2 commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
47be593
re-add token auth for analytics, enable SLA collection
ken-schneider 625d464
use single wrapper for basic and session calls
ken-schneider 189b2c4
remove comment
ken-schneider da837ec
fix test server url parsing
ken-schneider 5f3aa85
fix device ID test
ken-schneider 8200280
remove outdated comment
ken-schneider a0b49ab
Merge branch 'main' into ken/versa-sla-metrics-session-auth
ken-schneider 4cf720c
use initial GET for token
ken-schneider b660c8a
update test server routes
ken-schneider d8b3a25
use single call for analytics csrf/auth
ken-schneider 0b14520
remove unnecessary logs
ken-schneider 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
185 changes: 185 additions & 0 deletions
185
pkg/collector/corechecks/network-devices/versa/client/auth.go
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,185 @@ | ||
// Unless explicitly stated otherwise all files in this repository are licensed | ||
// under the Apache License Version 2.0. | ||
// This product includes software developed at Datadog (https://www.datadoghq.com/). | ||
// Copyright 2024-present Datadog, Inc. | ||
|
||
package client | ||
|
||
import ( | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"net/url" | ||
"strings" | ||
"time" | ||
|
||
"github.com/DataDog/datadog-agent/pkg/util/log" | ||
) | ||
|
||
// Login logs in to the Versa Director API, Versa Analytics API, gets CSRF | ||
// tokens, and a session cookie | ||
func (client *Client) login() error { | ||
authPayload := url.Values{} | ||
authPayload.Set("j_username", client.username) | ||
authPayload.Set("j_password", client.password) | ||
// this is a hack to get a CSRF token then | ||
// actually perform login | ||
// | ||
// ideally, we'd have a different endpoint to get a CSRF token | ||
// from at the very least | ||
err := client.runJSpringSecurityCheck(&authPayload) | ||
if err != nil { | ||
return fmt.Errorf("failed to run j_spring_security_check to get CSRF token: %w", err) | ||
} | ||
|
||
// now we can actually login and get a session cookie | ||
err = client.runJSpringSecurityCheck(&authPayload) | ||
if err != nil { | ||
return fmt.Errorf("failed to run j_spring_security_check to get session token: %w", err) | ||
} | ||
|
||
// Request to /versa/analytics/login to obtain Analytics CSRF prevention token | ||
analyticsPayload := url.Values{} | ||
analyticsPayload.Set("endpoint", client.analyticsEndpoint) // TODO: WHY? Where can we get this for others? | ||
|
||
// Run this request twice to get the CSRF token from analytics | ||
// the first succeeds but does not return the token | ||
err = client.runAnalyticsLogin(&analyticsPayload) | ||
if err != nil { | ||
return fmt.Errorf("failed to run analytics login: %w", err) | ||
} | ||
err = client.runAnalyticsLogin(&analyticsPayload) | ||
if err != nil { | ||
return fmt.Errorf("failed to get analytics CSRF token: %w", err) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// authenticate logins if no token or token is expired | ||
func (client *Client) authenticate() error { | ||
now := timeNow() | ||
|
||
client.authenticationMutex.Lock() | ||
defer client.authenticationMutex.Unlock() | ||
|
||
if client.token == "" || client.tokenExpiry.Before(now) { | ||
return client.login() | ||
} | ||
return nil | ||
} | ||
|
||
// clearAuth clears auth state | ||
func (client *Client) clearAuth() { | ||
client.authenticationMutex.Lock() | ||
client.token = "" | ||
client.authenticationMutex.Unlock() | ||
} | ||
|
||
// isAuthenticated determine if a request was successful from the headers | ||
// Vera can return HTTP 200 when auth is invalid, with the HTML login page | ||
// API calls returns application/json when successful. We assume receiving HTML means we're unauthenticated. | ||
func isAuthenticated(headers http.Header) bool { | ||
content := headers.Get("content-type") | ||
return !strings.HasPrefix(content, "text/html") | ||
} | ||
|
||
func (client *Client) runJSpringSecurityCheck(authPayload *url.Values) error { | ||
// TODO: this is pretty hacky at the moment, we're investigating | ||
// how to properly handle the CSRF token and see if we could just | ||
// use OAuth instead. For now, this gets us off the ground | ||
// | ||
// From support questions we've found, we might be able to perform a single | ||
// GET request, then perform analytics login | ||
|
||
// Request to /j_spring_security_check to obtain CSRF token and session cookie | ||
// TODO: use scheme from config, this is for testing | ||
req, err := client.newRequest("POST", "/versa/j_spring_security_check", strings.NewReader(authPayload.Encode()), true) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
// if we have a CSRF token, add it to the request | ||
if client.token != "" { | ||
req.Header.Add("X-CSRF-TOKEN", client.token) | ||
} | ||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded") | ||
sessionRes, err := client.httpClient.Do(req) | ||
if err != nil { | ||
return fmt.Errorf("invalid request: %w", err) | ||
} | ||
|
||
defer sessionRes.Body.Close() | ||
|
||
bodyBytes, err := io.ReadAll(sessionRes.Body) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
// TODO: remove this, we don't need it, just using it for debugging | ||
endpointURL, err := url.Parse(client.directorEndpoint + "/versa") | ||
if err != nil { | ||
return fmt.Errorf("url parsing failed: %w", err) | ||
} | ||
|
||
cookies := client.httpClient.Jar.Cookies(endpointURL) | ||
|
||
log.Tracef("Client login URL: %s", endpointURL) | ||
log.Tracef("Client login response headers: %+v", sessionRes.Header) | ||
for _, cookie := range cookies { | ||
log.Tracef("Versa Director cookie: %s=%s;Secure:%T", cookie.Name, cookie.Value, cookie.Secure) | ||
// TODO: replace with OAuth token | ||
if cookie.Name == "VD-CSRF-TOKEN" { | ||
client.token = cookie.Value | ||
client.tokenExpiry = timeNow().Add(time.Minute * 15) | ||
} | ||
} | ||
|
||
if sessionRes.StatusCode != 200 { | ||
return fmt.Errorf("authentication failed, status code: %v: %s", sessionRes.StatusCode, string(bodyBytes)) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (client *Client) runAnalyticsLogin(analyticsPayload *url.Values) error { | ||
// TODO: use proper client request creation, this is a testing work around | ||
req, err := client.newRequest("POST", "/versa/analytics/login", strings.NewReader(analyticsPayload.Encode()), true) | ||
if err != nil { | ||
return err | ||
} | ||
req.Header.Add("X-CSRF-TOKEN", client.token) | ||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded") | ||
|
||
loginRes, err := client.httpClient.Do(req) | ||
if err != nil { | ||
return fmt.Errorf("invalid request: %w", err) | ||
} | ||
|
||
defer loginRes.Body.Close() | ||
|
||
bodyBytes, err := io.ReadAll(loginRes.Body) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
endpointURL, err := url.Parse(client.directorEndpoint + "/versa") | ||
if err != nil { | ||
return fmt.Errorf("url parsing failed: %w", err) | ||
} | ||
|
||
cookies := client.httpClient.Jar.Cookies(endpointURL) | ||
|
||
log.Tracef("Client ANALYTICS login URL: %s", endpointURL) | ||
log.Tracef("Client ANALYTICS login response headers: %+v", loginRes.Header) | ||
for _, cookie := range cookies { | ||
log.Tracef("Versa Analytics cookie: %s=%s;Secure:%t;Path:%s", cookie.Name, cookie.Value, cookie.Secure, cookie.Path) | ||
} | ||
|
||
if loginRes.StatusCode != 200 { | ||
return fmt.Errorf("analytics authentication failed, status code: %v: %s", loginRes.StatusCode, string(bodyBytes)) | ||
} | ||
log.Tracef("Analytics login successful!! %s", string(bodyBytes)) | ||
|
||
return nil | ||
} |
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.
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.
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.
non blocking question
i forget if during our 1:1 you made a successful call to
/versa/login
that doesn't require thisendpoint
parameter but if i recall correctly, returns the token we're looking for?tbh it's very close to this
/versa/analytics/login
so maybe i mixed it up 🤔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 flow requires the analytics endpoint parameter to be set. I haven't been able to get that version working, it's something I'm going to look at when I get back.