Skip to content

Commit dd69997

Browse files
authored
Merge pull request #119 from deploymenttheory/dev
large refactor to decouple componants within the httpclient into dist…
2 parents b84499d + db8d75b commit dd69997

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+1017
-80
lines changed
+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// apiintegrations/apihandler/apihandler.go
2+
package apihandler
3+
4+
import (
5+
"net/http"
6+
7+
"github.com/deploymenttheory/go-api-http-client/apiintegrations/jamfpro"
8+
"github.com/deploymenttheory/go-api-http-client/apiintegrations/msgraph"
9+
"github.com/deploymenttheory/go-api-http-client/logger"
10+
"go.uber.org/zap"
11+
)
12+
13+
// APIHandler is an interface for encoding, decoding, and implenting contexual api functions for different API implementations.
14+
// It encapsulates behavior for encoding and decoding requests and responses.
15+
type APIHandler interface {
16+
ConstructAPIResourceEndpoint(instanceName string, endpointPath string, log logger.Logger) string
17+
ConstructAPIAuthEndpoint(instanceName string, endpointPath string, log logger.Logger) string
18+
MarshalRequest(body interface{}, method string, endpoint string, log logger.Logger) ([]byte, error)
19+
MarshalMultipartRequest(fields map[string]string, files map[string]string, log logger.Logger) ([]byte, string, error)
20+
HandleAPISuccessResponse(resp *http.Response, out interface{}, log logger.Logger) error
21+
HandleAPIErrorResponse(resp *http.Response, out interface{}, log logger.Logger) error
22+
GetContentTypeHeader(method string, log logger.Logger) string
23+
GetAcceptHeader() string
24+
GetDefaultBaseDomain() string
25+
GetOAuthTokenEndpoint() string
26+
GetBearerTokenEndpoint() string
27+
GetTokenRefreshEndpoint() string
28+
GetTokenInvalidateEndpoint() string
29+
GetAPIBearerTokenAuthenticationSupportStatus() bool
30+
GetAPIOAuthAuthenticationSupportStatus() bool
31+
GetAPIOAuthWithCertAuthenticationSupportStatus() bool
32+
GetAPIRequestHeaders(endpoint string) map[string]string // Provides standard headers required for making API requests.
33+
}
34+
35+
// LoadAPIHandler returns an APIHandler based on the provided API type.
36+
// 'apiType' parameter could be "jamf" or "graph" to specify which API handler to load.
37+
func LoadAPIHandler(apiType string, log logger.Logger) (APIHandler, error) {
38+
var apiHandler APIHandler
39+
switch apiType {
40+
case "jamfpro":
41+
apiHandler = &jamfpro.JamfAPIHandler{
42+
Logger: log,
43+
// Initialize with necessary parameters
44+
}
45+
log.Info("API handler loaded successfully", zap.String("APIType", apiType))
46+
47+
case "msgraph":
48+
apiHandler = &msgraph.GraphAPIHandler{
49+
// Initialize with necessary parameters
50+
}
51+
log.Info("API handler loaded successfully", zap.String("APIType", apiType))
52+
53+
default:
54+
return nil, log.Error("Unsupported API type", zap.String("APIType", apiType))
55+
}
56+
57+
return apiHandler, nil
58+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// apiintegrations/apihandler/apihandler_test.go
2+
package apihandler
3+
4+
import (
5+
"testing"
6+
7+
"github.com/deploymenttheory/go-api-http-client/apiintegrations/jamfpro"
8+
"github.com/deploymenttheory/go-api-http-client/apiintegrations/msgraph"
9+
"github.com/deploymenttheory/go-api-http-client/mocklogger"
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/mock"
12+
)
13+
14+
func TestLoadAPIHandler(t *testing.T) {
15+
// Create a mock logger for testing purposes.
16+
mockLog := mocklogger.NewMockLogger()
17+
18+
// Define your test cases.
19+
tests := []struct {
20+
name string
21+
apiType string
22+
wantType interface{}
23+
wantErr bool
24+
}{
25+
{
26+
name: "Load JamfPro Handler",
27+
apiType: "jamfpro",
28+
wantType: &jamfpro.JamfAPIHandler{},
29+
wantErr: false,
30+
},
31+
{
32+
name: "Load Graph Handler",
33+
apiType: "msgraph",
34+
wantType: &msgraph.GraphAPIHandler{},
35+
wantErr: false,
36+
},
37+
{
38+
name: "Unsupported API Type",
39+
apiType: "unknown",
40+
wantErr: true,
41+
},
42+
}
43+
44+
for _, tt := range tests {
45+
t.Run(tt.name, func(t *testing.T) {
46+
// Setup expectations for the mock logger based on whether an error is expected.
47+
if tt.wantErr {
48+
mockLog.On("Error", mock.Anything, mock.Anything, mock.Anything).Return().Once()
49+
} else {
50+
mockLog.On("Info", mock.Anything, mock.Anything, mock.Anything).Return().Once()
51+
}
52+
53+
// Attempt to load the API handler.
54+
got, err := LoadAPIHandler(tt.apiType, mockLog)
55+
56+
// Assert error handling.
57+
if tt.wantErr {
58+
assert.Error(t, err)
59+
} else {
60+
assert.NoError(t, err)
61+
assert.IsType(t, tt.wantType, got, "Got %T, want %T", got, tt.wantType)
62+
}
63+
64+
// Assert that the mock logger's expectations were met.
65+
mockLog.AssertExpectations(t)
66+
})
67+
}
68+
}
File renamed without changes.
+108
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// authenticationhandler/auth_bearer_token.go
2+
/* The http_client_auth package focuses on authentication mechanisms for an HTTP client.
3+
It provides structures and methods for handling both basic and bearer token based authentication
4+
*/
5+
6+
package authenticationhandler
7+
8+
import (
9+
"encoding/json"
10+
"fmt"
11+
"net/http"
12+
"time"
13+
14+
"github.com/deploymenttheory/go-api-http-client/apiintegrations/apihandler"
15+
"go.uber.org/zap"
16+
)
17+
18+
// ObtainToken fetches and sets an authentication token using the stored basic authentication credentials.
19+
func (h *AuthTokenHandler) ObtainToken(apiHandler apihandler.APIHandler, httpClient *http.Client, username string, password string) error {
20+
21+
// Use the APIHandler's method to get the bearer token endpoint
22+
bearerTokenEndpoint := apiHandler.GetBearerTokenEndpoint()
23+
24+
// Construct the full authentication endpoint URL
25+
authenticationEndpoint := apiHandler.ConstructAPIAuthEndpoint(h.InstanceName, bearerTokenEndpoint, h.Logger)
26+
27+
h.Logger.Debug("Attempting to obtain token for user", zap.String("Username", username))
28+
29+
req, err := http.NewRequest("POST", authenticationEndpoint, nil)
30+
if err != nil {
31+
h.Logger.LogError("authentication_request_creation_error", "POST", authenticationEndpoint, 0, "", err, "Failed to create new request for token")
32+
return err
33+
}
34+
req.SetBasicAuth(username, password)
35+
36+
resp, err := httpClient.Do(req)
37+
if err != nil {
38+
h.Logger.LogError("authentication_request_error", "POST", authenticationEndpoint, resp.StatusCode, resp.Status, err, "Failed to make request for token")
39+
return err
40+
}
41+
defer resp.Body.Close()
42+
43+
if resp.StatusCode != http.StatusOK {
44+
h.Logger.LogError("token_authentication_failed", "POST", authenticationEndpoint, resp.StatusCode, resp.Status, fmt.Errorf("authentication failed with status code: %d", resp.StatusCode), "Token acquisition attempt resulted in a non-OK response")
45+
return fmt.Errorf("received non-OK response status: %d", resp.StatusCode)
46+
}
47+
48+
tokenResp := &TokenResponse{}
49+
err = json.NewDecoder(resp.Body).Decode(tokenResp)
50+
if err != nil {
51+
h.Logger.Error("Failed to decode token response", zap.Error(err))
52+
return err
53+
}
54+
55+
h.Token = tokenResp.Token
56+
h.Expires = tokenResp.Expires
57+
tokenDuration := time.Until(h.Expires)
58+
59+
h.Logger.Info("Token obtained successfully", zap.Time("Expiry", h.Expires), zap.Duration("Duration", tokenDuration))
60+
61+
return nil
62+
}
63+
64+
// RefreshToken refreshes the current authentication token.
65+
func (h *AuthTokenHandler) RefreshToken(apiHandler apihandler.APIHandler, httpClient *http.Client) error {
66+
h.tokenLock.Lock()
67+
defer h.tokenLock.Unlock()
68+
69+
// Use the APIHandler's method to get the token refresh endpoint
70+
apiTokenRefreshEndpoint := apiHandler.GetTokenRefreshEndpoint()
71+
72+
// Construct the full authentication endpoint URL
73+
tokenRefreshEndpoint := apiHandler.ConstructAPIAuthEndpoint(h.InstanceName, apiTokenRefreshEndpoint, h.Logger)
74+
75+
h.Logger.Debug("Attempting to refresh token", zap.String("URL", tokenRefreshEndpoint))
76+
77+
req, err := http.NewRequest("POST", tokenRefreshEndpoint, nil)
78+
if err != nil {
79+
h.Logger.Error("Failed to create new request for token refresh", zap.Error(err))
80+
return err
81+
}
82+
req.Header.Add("Authorization", "Bearer "+h.Token)
83+
84+
resp, err := httpClient.Do(req)
85+
if err != nil {
86+
h.Logger.Error("Failed to make request for token refresh", zap.Error(err))
87+
return err
88+
}
89+
defer resp.Body.Close()
90+
91+
if resp.StatusCode != http.StatusOK {
92+
h.Logger.Warn("Token refresh response status is not OK", zap.Int("StatusCode", resp.StatusCode))
93+
return fmt.Errorf("token refresh failed with status code: %d", resp.StatusCode)
94+
}
95+
96+
tokenResp := &TokenResponse{}
97+
err = json.NewDecoder(resp.Body).Decode(tokenResp)
98+
if err != nil {
99+
h.Logger.Error("Failed to decode token response", zap.Error(err))
100+
return err
101+
}
102+
103+
h.Token = tokenResp.Token
104+
h.Expires = tokenResp.Expires
105+
h.Logger.Info("Token refreshed successfully", zap.Time("Expiry", tokenResp.Expires))
106+
107+
return nil
108+
}

authenticationhandler/auth_handler.go

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// authenticationhandler/auth_handler.go
2+
3+
package authenticationhandler
4+
5+
import (
6+
"sync"
7+
"time"
8+
9+
"github.com/deploymenttheory/go-api-http-client/logger"
10+
)
11+
12+
// AuthTokenHandler manages authentication tokens.
13+
type AuthTokenHandler struct {
14+
Credentials ClientCredentials // Credentials holds the authentication credentials.
15+
Token string // Token holds the current authentication token.
16+
Expires time.Time // Expires indicates the expiry time of the current authentication token.
17+
Logger logger.Logger // Logger provides structured logging capabilities for logging information, warnings, and errors.
18+
AuthMethod string // AuthMethod specifies the method of authentication, e.g., "bearer" or "oauth".
19+
InstanceName string // InstanceName represents the name of the instance or environment the client is interacting with.
20+
tokenLock sync.Mutex // tokenLock ensures thread-safe access to the token and its expiry to prevent concurrent write/read issues.
21+
HideSensitiveData bool
22+
}
23+
24+
// ClientCredentials holds the credentials necessary for authentication.
25+
type ClientCredentials struct {
26+
Username string
27+
Password string
28+
ClientID string
29+
ClientSecret string
30+
}
31+
32+
// TokenResponse represents the structure of a token response from the API.
33+
type TokenResponse struct {
34+
Token string `json:"token"`
35+
Expires time.Time `json:"expires"`
36+
}
37+
38+
// NewAuthTokenHandler creates a new instance of AuthTokenHandler.
39+
func NewAuthTokenHandler(logger logger.Logger, authMethod string, credentials ClientCredentials, instanceName string, hideSensitiveData bool) *AuthTokenHandler {
40+
return &AuthTokenHandler{
41+
Logger: logger,
42+
AuthMethod: authMethod,
43+
Credentials: credentials,
44+
InstanceName: instanceName,
45+
HideSensitiveData: hideSensitiveData,
46+
}
47+
}

authenticationhandler/auth_oauth.go

+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// authenticationhandler/auth_oauth.go
2+
3+
/* The http_client_auth package focuses on authentication mechanisms for an HTTP client.
4+
It provides structures and methods for handling OAuth-based authentication
5+
*/
6+
7+
package authenticationhandler
8+
9+
import (
10+
"bytes"
11+
"encoding/json"
12+
"fmt"
13+
"io"
14+
"net/http"
15+
"net/url"
16+
"strings"
17+
"time"
18+
19+
"github.com/deploymenttheory/go-api-http-client/apiintegrations/apihandler"
20+
"github.com/deploymenttheory/go-api-http-client/headers"
21+
"go.uber.org/zap"
22+
)
23+
24+
// OAuthResponse represents the response structure when obtaining an OAuth access token.
25+
type OAuthResponse struct {
26+
AccessToken string `json:"access_token"` // AccessToken is the token that can be used in subsequent requests for authentication.
27+
ExpiresIn int64 `json:"expires_in"` // ExpiresIn specifies the duration in seconds after which the access token expires.
28+
TokenType string `json:"token_type"` // TokenType indicates the type of token, typically "Bearer".
29+
RefreshToken string `json:"refresh_token,omitempty"` // RefreshToken is used to obtain a new access token when the current one expires.
30+
Error string `json:"error,omitempty"` // Error contains details if an error occurs during the token acquisition process.
31+
}
32+
33+
// ObtainOAuthToken fetches an OAuth access token using the provided client ID and client secret.
34+
// It updates the AuthTokenHandler's Token and Expires fields with the obtained values.
35+
func (h *AuthTokenHandler) ObtainOAuthToken(apiHandler apihandler.APIHandler, httpClient *http.Client, clientID, clientSecret string) error {
36+
// Use the APIHandler's method to get the OAuth token endpoint
37+
oauthTokenEndpoint := apiHandler.GetOAuthTokenEndpoint()
38+
39+
// Construct the full authentication endpoint URL
40+
authenticationEndpoint := apiHandler.ConstructAPIAuthEndpoint(h.InstanceName, oauthTokenEndpoint, h.Logger)
41+
42+
data := url.Values{}
43+
data.Set("client_id", clientID)
44+
data.Set("client_secret", clientSecret)
45+
data.Set("grant_type", "client_credentials")
46+
47+
h.Logger.Debug("Attempting to obtain OAuth token", zap.String("ClientID", clientID))
48+
49+
req, err := http.NewRequest("POST", authenticationEndpoint, strings.NewReader(data.Encode()))
50+
if err != nil {
51+
h.Logger.Error("Failed to create request for OAuth token", zap.Error(err))
52+
return err
53+
}
54+
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
55+
56+
resp, err := httpClient.Do(req)
57+
if err != nil {
58+
h.Logger.Error("Failed to execute request for OAuth token", zap.Error(err))
59+
return err
60+
}
61+
defer resp.Body.Close()
62+
63+
bodyBytes, err := io.ReadAll(resp.Body)
64+
if err != nil {
65+
h.Logger.Error("Failed to read response body", zap.Error(err))
66+
return err
67+
}
68+
69+
// Reset the response body to its original state
70+
resp.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
71+
72+
oauthResp := &OAuthResponse{}
73+
err = json.Unmarshal(bodyBytes, oauthResp)
74+
if err != nil {
75+
h.Logger.Error("Failed to decode OAuth response", zap.Error(err))
76+
return err
77+
}
78+
79+
if oauthResp.Error != "" {
80+
h.Logger.Error("Error obtaining OAuth token", zap.String("Error", oauthResp.Error))
81+
return fmt.Errorf("error obtaining OAuth token: %s", oauthResp.Error)
82+
}
83+
84+
if oauthResp.AccessToken == "" {
85+
h.Logger.Error("Empty access token received")
86+
return fmt.Errorf("empty access token received")
87+
}
88+
89+
expiresIn := time.Duration(oauthResp.ExpiresIn) * time.Second
90+
expirationTime := time.Now().Add(expiresIn)
91+
92+
// Modified log call using the helper function
93+
redactedAccessToken := headers.RedactSensitiveHeaderData(h.HideSensitiveData, "AccessToken", oauthResp.AccessToken)
94+
h.Logger.Info("OAuth token obtained successfully", zap.String("AccessToken", redactedAccessToken), zap.Duration("ExpiresIn", expiresIn), zap.Time("ExpirationTime", expirationTime))
95+
96+
h.Token = oauthResp.AccessToken
97+
h.Expires = expirationTime
98+
99+
return nil
100+
}

0 commit comments

Comments
 (0)