diff --git a/go.mod b/go.mod index 568a3a0..b1bae18 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,5 @@ -module github.com/deploymenttheory/go-api-http-client-integrations/jamfpro/jamfprointegration +module github.com/deploymenttheory/go-api-http-client-integrations + go 1.22.4 diff --git a/jamfpro/jamfprointegration/auth.go b/jamfpro/jamfprointegration/auth.go index 58ee3f9..758e51f 100644 --- a/jamfpro/jamfprointegration/auth.go +++ b/jamfpro/jamfprointegration/auth.go @@ -21,6 +21,20 @@ type authInterface interface { tokenEmpty() bool } +// checkRefreshToken checks and refreshes the authentication token if necessary. +// This function ensures that the authentication token is valid and not expired. If the token is empty, expired, +// or within the buffer period before expiry, it attempts to obtain a new token and validates the new token's lifetime +// against the buffer period to prevent infinite loops. +// +// Returns: +// - error: Any error encountered during the token refresh process or if the token's lifetime is shorter than the buffer period. Returns nil if no errors occur. +// +// Functionality: +// - Logs a warning if the token is empty. +// - Checks if the token is expired, within the buffer period, or empty. +// - Attempts to obtain a new token if the current token is invalid. +// - Validates the new token's lifetime against the buffer period to prevent bad token lifetime/buffer combinations. +// - Returns an error if the token refresh fails or if the new token's lifetime is shorter than the buffer period. func (j *Integration) checkRefreshToken() error { var err error diff --git a/jamfpro/jamfprointegration/auth_basic.go b/jamfpro/jamfprointegration/auth_basic.go index 50a4edf..7da3475 100644 --- a/jamfpro/jamfprointegration/auth_basic.go +++ b/jamfpro/jamfprointegration/auth_basic.go @@ -31,7 +31,21 @@ type basicAuthResponse struct { // Operations -// TODO comment +// getNewToken obtains a new bearer token from the authentication server. +// This function constructs a new HTTP request to the bearer token endpoint using the basic authentication credentials, +// sends the request, and updates the basicAuth instance with the new bearer token and its expiry time. +// +// Returns: +// - error: Any error encountered during the request, response handling, or JSON decoding. Returns nil if no errors occur. +// +// Functionality: +// - Constructs the complete bearer token endpoint URL. +// - Creates a new HTTP POST request and sets the basic authentication headers. +// - Sends the request using an HTTP client and checks the response status. +// - Decodes the JSON response to obtain the bearer token and its expiry time. +// - Updates the basicAuth instance with the new bearer token and its expiry time. +// - Logs the successful token retrieval with the expiry time and duration. +// // TODO migrate strings func (a *basicAuth) getNewToken() error { client := http.Client{} @@ -70,24 +84,40 @@ func (a *basicAuth) getNewToken() error { return nil } -// TODO comment +// getTokenString returns the current bearer token as a string. +// This function provides access to the current bearer token stored in the basicAuth instance. +// +// Returns: +// - string: The current bearer token. func (a *basicAuth) getTokenString() string { return a.bearerToken } -// TODO comment +// getExpiryTime returns the expiry time of the current bearer token. +// This function provides access to the expiry time of the current bearer token stored in the basicAuth instance. +// +// Returns: +// - time.Time: The expiry time of the current bearer token. func (a *basicAuth) getExpiryTime() time.Time { return a.bearerTokenExpiryTime } // Utils -// TODO comment +// tokenExpired checks if the current bearer token has expired. +// This function compares the current time with the bearer token's expiry time to determine if the token has expired. +// +// Returns: +// - bool: True if the bearer token has expired, false otherwise. func (a *basicAuth) tokenExpired() bool { return a.bearerTokenExpiryTime.Before(time.Now()) } -// TODO comment +// tokenInBuffer checks if the current bearer token is within the buffer period before expiry. +// This function calculates the remaining time until the token expires and compares it with the buffer period. +// +// Returns: +// - bool: True if the bearer token is within the buffer period, false otherwise. func (a *basicAuth) tokenInBuffer() bool { if time.Until(a.bearerTokenExpiryTime) <= a.bufferPeriod { return true @@ -96,7 +126,11 @@ func (a *basicAuth) tokenInBuffer() bool { return false } -// TODO comment +// tokenEmpty checks if the current bearer token is empty. +// This function determines if the bearer token string stored in the basicAuth instance is empty. +// +// Returns: +// - bool: True if the bearer token is empty, false otherwise. func (a *basicAuth) tokenEmpty() bool { if a.bearerToken == "" { return true diff --git a/jamfpro/jamfprointegration/headers.go b/jamfpro/jamfprointegration/headers.go index ed6dc86..609655d 100644 --- a/jamfpro/jamfprointegration/headers.go +++ b/jamfpro/jamfprointegration/headers.go @@ -53,6 +53,7 @@ func (j *Integration) getAcceptHeader() string { return weightedAcceptHeader } +// getUserAgentHeader returns the User-Agent header string for the Jamf Pro API. func (j *Integration) getUserAgentHeader() string { return "go-api-http-client-jamfpro-integration" } diff --git a/jamfpro/jamfprointegration/marshalling.go b/jamfpro/jamfprointegration/marshall.go similarity index 64% rename from jamfpro/jamfprointegration/marshalling.go rename to jamfpro/jamfprointegration/marshall.go index e1aed2d..05c5622 100644 --- a/jamfpro/jamfprointegration/marshalling.go +++ b/jamfpro/jamfprointegration/marshall.go @@ -1,4 +1,4 @@ -// jamfpro_api_request.go +// jamfpro/jamfprointegration/marshall.go package jamfprointegration import ( @@ -11,10 +11,30 @@ import ( "path/filepath" "strings" + "github.com/deploymenttheory/go-api-http-client-integrations/shared/helpers" "go.uber.org/zap" ) -// MarshalRequest encodes the request body according to the endpoint for the API. +// MarshalRequest encodes the request body according to the endpoint for the Jamf Pro API. +// This function marshals the request body as JSON or XML based on the endpoint string. +// It takes an interface{} type body, an HTTP method, and an endpoint as input, and returns the +// marshaled byte slice along with any error encountered during marshaling. +// +// Parameters: +// - body: The request body to be marshaled, of type interface{}. +// - method: The HTTP method being used for the request (e.g., "POST", "PUT", "PATCH"). +// - endpoint: The API endpoint for the request. +// +// Returns: +// - []byte: The marshaled byte slice of the request body. +// - error: Any error encountered during the marshaling process. +// +// Functionality: +// - Determines the format (JSON or XML) based on the endpoint string. +// - Marshals the body as JSON if the endpoint contains "/api" or as XML if it contains "/JSSResource". +// - Logs the marshaled request body for POST, PUT, and PATCH methods using the integrated logger. +// - Logs an error if marshaling fails and returns the error. +// - Returns an error if the format is invalid. func (j *Integration) marshalRequest(body interface{}, method string, endpoint string) ([]byte, error) { var ( data []byte @@ -74,7 +94,7 @@ func (j *Integration) marshalMultipartRequest(fields map[string]string, files ma } for formField, filePath := range files { - file, err := SafeOpenFile(filePath) + file, err := helpers.SafeOpenFile(filePath) if err != nil { j.Logger.Error("Failed to open file securely", zap.String("file", filePath), zap.Error(err)) return nil, "", err diff --git a/jamfpro/jamfprointegration/request.go b/jamfpro/jamfprointegration/request.go index 3d3b6bb..6f8dc44 100644 --- a/jamfpro/jamfprointegration/request.go +++ b/jamfpro/jamfprointegration/request.go @@ -5,7 +5,22 @@ import ( "net/http" ) -// TODO func comment +// prepRequest prepares an HTTP request by setting the necessary headers and handling authorization. +// This function adds headers for Accept, Content-Type, User-Agent, and Authorization based on the Integration's methods +// and checks for token refresh if needed. +// +// Parameters: +// - req: A pointer to the http.Request that needs to be prepared. +// +// Returns: +// - error: Any error encountered while checking the refresh token or setting headers. Returns nil if no errors occur. +// +// Functionality: +// - Adds an "Accept" header based on the Integration's getAcceptHeader method. +// - Adds a "Content-Type" header based on the Integration's getContentTypeHeader method, which depends on the request URL. +// - Adds a "User-Agent" header based on the Integration's getUserAgentHeader method. +// - Checks and refreshes the token if necessary using the Integration's checkRefreshToken method. +// - Adds an "Authorization" header with a Bearer token obtained from the Integration's auth.getTokenString method. func (j *Integration) prepRequest(req *http.Request) error { req.Header.Add("Accept", j.getAcceptHeader()) req.Header.Add("Content-Type", j.getContentTypeHeader(req.URL.String())) diff --git a/jamfpro/jamfprointegration/urls.go b/jamfpro/jamfprointegration/urls.go index a5e75dc..84af565 100644 --- a/jamfpro/jamfprointegration/urls.go +++ b/jamfpro/jamfprointegration/urls.go @@ -1,8 +1,7 @@ -// jamfpro_api_url.go +// jamfpro/jamfprointegration/urls.go package jamfprointegration -// SetBaseDomain returns the appropriate base domain for URL construction. -// It uses j.OverrideBaseDomain if set, otherwise falls back to DefaultBaseDomain. +// GetBaseDomain returns the base domain for the Jamf Pro integration. func (j *Integration) GetBaseDomain() string { return j.BaseDomain } diff --git a/msgraph/msgraphintegration/auth.go b/msgraph/msgraphintegration/auth.go new file mode 100644 index 0000000..691ec5d --- /dev/null +++ b/msgraph/msgraphintegration/auth.go @@ -0,0 +1,63 @@ +// msgraph/msgraphintegration/auth.go +package msgraphintegration + +import ( + "errors" + "time" +) + +const ( + tokenEmptyWarnString = "token empty before processing - disregard if first run" +) + +// authInterface defines the methods required to satify the authentication interface. +type authInterface interface { + // Token Operations + getNewToken() error + getTokenString() string + getExpiryTime() time.Time + + // Token Utils + tokenExpired() bool + tokenInBuffer() bool + tokenEmpty() bool +} + +// checkRefreshToken checks and refreshes the authentication token if necessary. +// This function ensures that the authentication token is valid and not expired. If the token is empty, expired, +// or within the buffer period before expiry, it attempts to obtain a new token and validates the new token's lifetime +// against the buffer period to prevent infinite loops. +// +// Returns: +// - error: Any error encountered during the token refresh process or if the token's lifetime is shorter than the buffer period. Returns nil if no errors occur. +// +// Functionality: +// - Logs a warning if the token is empty. +// - Checks if the token is expired, within the buffer period, or empty. +// - Attempts to obtain a new token if the current token is invalid. +// - Validates the new token's lifetime against the buffer period to prevent bad token lifetime/buffer combinations. +// - Returns an error if the token refresh fails or if the new token's lifetime is shorter than the buffer period. +func (m *Integration) checkRefreshToken() error { + var err error + + if m.auth.tokenEmpty() { + m.Logger.Warn(tokenEmptyWarnString) + } + + if m.auth.tokenExpired() || m.auth.tokenInBuffer() || m.auth.tokenEmpty() { + err = m.auth.getNewToken() + + if err != nil { + return err + } + + // Protects against bad token lifetime/buffer combinations (infinite loops) + if m.auth.tokenExpired() || m.auth.tokenInBuffer() { + return errors.New("token lifetime is shorter than buffer period. please adjust parameters") + } + + return nil + } + + return nil +} diff --git a/msgraph/msgraphintegration/auth_basic.go b/msgraph/msgraphintegration/auth_basic.go new file mode 100644 index 0000000..6458a6d --- /dev/null +++ b/msgraph/msgraphintegration/auth_basic.go @@ -0,0 +1,151 @@ +// msgraph/msgraphintegration/auth_basic.go +package msgraphintegration + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + "strings" + "time" + + "github.com/deploymenttheory/go-api-http-client/logger" + "go.uber.org/zap" +) + +type basicAuth struct { + // Set + baseDomain string + username string + password string + tenantID string + bufferPeriod time.Duration + logger logger.Logger + + // Computed + basicToken string + bearerToken string + bearerTokenExpiryTime time.Time +} + +type basicAuthResponse struct { + Token string `json:"token"` + Expires time.Time `json:"expires"` +} + +// Operations + +// getNewToken obtains a new bearer token from the Microsoft Graph API authentication server. +// This function constructs a new HTTP request to the OAuth2.0 token endpoint using the basic authentication credentials, +// sends the request, and updates the basicAuth instance with the new bearer token and its expiry time. +// +// Returns: +// - error: Any error encountered during the request, response handling, or JSON decoding. Returns nil if no errors occur. +// +// Functionality: +// - Constructs the complete OAuth2.0 token endpoint URL using the tenantID. +// - Logs the constructed authentication URL. +// - Creates a new HTTP POST request and sets the form data with grant type, scope, username, and password for the request body. +// - Sends the request using an HTTP client and checks the response status. +// - Decodes the JSON response to obtain the bearer token and its expiry time. +// - Updates the basicAuth instance with the new bearer token and its expiry time. +// - Logs the successful token retrieval with the expiry time and duration. +func (a *basicAuth) getNewToken() error { + client := http.Client{} + + constructedBearerAuthEndpoint := fmt.Sprintf("%s/%s%s", baseAuthURL, a.tenantID, oAuthTokenEndpoint) + + a.logger.Info("constructed Microsoft Graph API authentication URL", zap.String("URL", constructedBearerAuthEndpoint)) + + formData := url.Values{ + "grant_type": {"password"}, + "scope": {oAuthTokenScope}, + "username": {a.username}, + "password": {a.password}, + } + + req, err := http.NewRequest("POST", constructedBearerAuthEndpoint, strings.NewReader(formData.Encode())) + if err != nil { + return err + } + + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + resp, err := client.Do(req) + if err != nil { + return err + } + + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("received non-OK response status: %d", resp.StatusCode) + } + + tokenResp := &basicAuthResponse{} + err = json.NewDecoder(resp.Body).Decode(tokenResp) + if err != nil { + return err + } + + a.bearerToken = tokenResp.Token + a.bearerTokenExpiryTime = tokenResp.Expires + tokenDuration := time.Until(a.bearerTokenExpiryTime) + + a.logger.Info("Token obtained successfully", zap.Time("Expiry", a.bearerTokenExpiryTime), zap.Duration("Duration", tokenDuration)) + + return nil +} + +// getTokenString returns the current bearer token as a string. +// This function provides access to the current bearer token stored in the basicAuth instance. +// +// Returns: +// - string: The current bearer token. +func (a *basicAuth) getTokenString() string { + return a.bearerToken +} + +// getExpiryTime returns the expiry time of the current bearer token. +// This function provides access to the expiry time of the current bearer token stored in the basicAuth instance. +// +// Returns: +// - time.Time: The expiry time of the current bearer token. +func (a *basicAuth) getExpiryTime() time.Time { + return a.bearerTokenExpiryTime +} + +// Utils + +// tokenExpired checks if the current bearer token has expired. +// This function compares the current time with the bearer token's expiry time to determine if the token has expired. +// +// Returns: +// - bool: True if the bearer token has expired, false otherwise. +func (a *basicAuth) tokenExpired() bool { + return a.bearerTokenExpiryTime.Before(time.Now()) +} + +// tokenInBuffer checks if the current bearer token is within the buffer period before expiry. +// This function calculates the remaining time until the token expires and compares it with the buffer period. +// +// Returns: +// - bool: True if the bearer token is within the buffer period, false otherwise. +func (a *basicAuth) tokenInBuffer() bool { + if time.Until(a.bearerTokenExpiryTime) <= a.bufferPeriod { + return true + } + + return false +} + +// tokenEmpty checks if the current bearer token is empty. +// This function determines if the bearer token string stored in the basicAuth instance is empty. +// +// Returns: +// - bool: True if the bearer token is empty, false otherwise. +func (a *basicAuth) tokenEmpty() bool { + if a.bearerToken == "" { + return true + } + return false +} diff --git a/msgraph/msgraphintegration/auth_oauth.go b/msgraph/msgraphintegration/auth_oauth.go new file mode 100644 index 0000000..3d7a071 --- /dev/null +++ b/msgraph/msgraphintegration/auth_oauth.go @@ -0,0 +1,149 @@ +// msgraph/msgraphintegration/auth_oauth.go +package msgraphintegration + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "time" + + "github.com/deploymenttheory/go-api-http-client/logger" +) + +type oauth struct { + // Set + clientId string + clientSecret string + tenantID string + bufferPeriod time.Duration + Logger logger.Logger + + // Computed + expiryTime time.Time + token string +} + +// OAuthResponse represents the response structure when obtaining an OAuth access token from Microsoft Graph. +type OAuthResponse struct { + AccessToken string `json:"access_token"` + ExpiresIn int64 `json:"expires_in"` + TokenType string `json:"token_type"` +} + +// Operations + +// getNewToken obtains a new bearer token from the Microsoft Graph authentication server. +// This function constructs a new HTTP request to the OAuth2.0 token endpoint using the client credentials, +// sends the request, and updates the oauth instance with the new bearer token and its expiry time. +// +// Returns: +// - error: Any error encountered during the request, response handling, or JSON decoding. Returns nil if no errors occur. +// +// Functionality: +// - Constructs the complete OAuth2.0 token endpoint URL using the tenantID. +// - Creates a new HTTP POST request and sets the form data with client ID, client secret, and grant type. +// - Sends the request using an HTTP client and checks the response status. +// - Decodes the JSON response to obtain the bearer token and its expiry time. +// - Updates the oauth instance with the new bearer token and its expiry time. +func (a *oauth) getNewToken() error { + client := http.Client{} + data := url.Values{} + + data.Set("client_id", a.clientId) + data.Set("client_secret", a.clientSecret) + data.Set("grant_type", "client_credentials") + data.Set("scope", oAuthTokenScope) + + oauthCompleteEndpoint := fmt.Sprintf("%s/%s%s", baseAuthURL, a.tenantID, oAuthTokenEndpoint) + req, err := http.NewRequest("POST", oauthCompleteEndpoint, strings.NewReader(data.Encode())) + if err != nil { + return err + } + + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + + resp, err := client.Do(req) + if err != nil { + return err + } + + if resp.StatusCode < 200 || resp.StatusCode > 299 { + return fmt.Errorf("bad request: %v", resp) + } + + defer resp.Body.Close() + + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + + resp.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) + + oauthResp := &OAuthResponse{} + err = json.Unmarshal(bodyBytes, oauthResp) + if err != nil { + return fmt.Errorf("failed to decode OAuth response: %w", err) + } + + if oauthResp.AccessToken == "" { + return fmt.Errorf("empty access token received") + } + + expiresIn := time.Duration(oauthResp.ExpiresIn) * time.Second + a.expiryTime = time.Now().Add(expiresIn) + a.token = oauthResp.AccessToken + + return nil +} + +// getTokenString returns the current bearer token as a string. +// This function provides access to the current bearer token stored in the oauth instance. +// +// Returns: +// - string: The current bearer token. +func (a *oauth) getTokenString() string { + return a.token +} + +// getExpiryTime returns the expiry time of the current bearer token. +// This function provides access to the expiry time of the current bearer token stored in the oauth instance. +// +// Returns: +// - time.Time: The expiry time of the current bearer token. +func (a *oauth) getExpiryTime() time.Time { + return a.expiryTime +} + +// Utils + +// tokenExpired checks if the current bearer token has expired. +// This function compares the current time with the bearer token's expiry time to determine if the token has expired. +// +// Returns: +// - bool: True if the bearer token has expired, false otherwise. +func (a *oauth) tokenExpired() bool { + return a.expiryTime.Before(time.Now()) +} + +// tokenInBuffer checks if the current bearer token is within the buffer period before expiry. +// This function calculates the remaining time until the token expires and compares it with the buffer period. +// +// Returns: +// - bool: True if the bearer token is within the buffer period, false otherwise. +func (a *oauth) tokenInBuffer() bool { + return time.Until(a.expiryTime) <= a.bufferPeriod +} + +// tokenEmpty checks if the current bearer token is empty. +// This function determines if the bearer token string stored in the oauth instance is empty. +// +// Returns: +// - bool: True if the bearer token is empty, false otherwise. +func (a *oauth) tokenEmpty() bool { + return a.token == "" +} diff --git a/msgraph/msgraphintegration/builders.go b/msgraph/msgraphintegration/builders.go new file mode 100644 index 0000000..a2197a8 --- /dev/null +++ b/msgraph/msgraphintegration/builders.go @@ -0,0 +1,100 @@ +// msgraph/msgraphintegration/builders.go +package msgraphintegration + +import ( + "time" + + "github.com/deploymenttheory/go-api-http-client/logger" +) + +// BuildIntegrationWithOAuth constructs an Integration instance using OAuth2.0 authentication. +// It sets up the OAuth2.0 authentication method with the provided client ID, client secret, and tenant ID. +// Checks the token refresh status upon creation. +// +// Parameters: +// - logger: A logger instance for logging purposes. +// - bufferPeriod: The buffer period before token expiry to refresh the token. +// - clientId: The client ID for OAuth2.0 authentication. +// - clientSecret: The client secret for OAuth2.0 authentication. +// - tenantID: The tenant ID for the Microsoft Graph API. +// +// Returns: +// - *Integration: A pointer to the constructed Integration instance. +// - error: Any error encountered during the token refresh check. +func BuildIntegrationWithOAuth(logger logger.Logger, bufferPeriod time.Duration, clientId string, clientSecret string, tenantID string) (*Integration, error) { + integration := &Integration{ + TenantID: tenantID, + Logger: logger, + AuthMethodDescriptor: "oauth2", + } + + integration.BuildOAuth(clientId, clientSecret, bufferPeriod, tenantID) + err := integration.CheckRefreshToken() + + return integration, err +} + +// BuildIntegrationWithBasicAuth constructs an Integration instance using Basic Authentication. +// It sets up the basic authentication method with the provided username, password, and tenant ID. +// Checks the token refresh status upon creation. +// +// Parameters: +// - logger: A logger instance for logging purposes. +// - bufferPeriod: The buffer period before token expiry to refresh the token. +// - username: The username for basic authentication. +// - password: The password for basic authentication. +// - tenantID: The tenant ID for the Microsoft Graph API. +// +// Returns: +// - *Integration: A pointer to the constructed Integration instance. +// - error: Any error encountered during the token refresh check. +func BuildIntegrationWithBasicAuth(logger logger.Logger, bufferPeriod time.Duration, username string, password string, tenantID string) (*Integration, error) { + integration := &Integration{ + TenantID: tenantID, + Logger: logger, + AuthMethodDescriptor: "basic", + } + + integration.BuildBasicAuth(username, password, bufferPeriod, tenantID) + err := integration.CheckRefreshToken() + + return integration, err +} + +// BuildOAuth sets up the OAuth2.0 authentication method for the Integration instance. +// +// Parameters: +// - clientId: The client ID for OAuth2.0 authentication. +// - clientSecret: The client secret for OAuth2.0 authentication. +// - bufferPeriod: The buffer period before token expiry to refresh the token. +// - tenantID: The tenant ID for the Microsoft Graph API. +func (m *Integration) BuildOAuth(clientId string, clientSecret string, bufferPeriod time.Duration, tenantID string) { + authInterface := &oauth{ + clientId: clientId, + clientSecret: clientSecret, + bufferPeriod: bufferPeriod, + Logger: m.Logger, + tenantID: tenantID, + } + + m.auth = authInterface +} + +// BuildBasicAuth sets up the basic authentication method for the Integration instance. +// +// Parameters: +// - username: The username for basic authentication. +// - password: The password for basic authentication. +// - bufferPeriod: The buffer period before token expiry to refresh the token. +// - tenantID: The tenant ID for the Microsoft Graph API. +func (j *Integration) BuildBasicAuth(username string, password string, bufferPeriod time.Duration, tenantID string) { + authInterface := &basicAuth{ + username: username, + password: password, + bufferPeriod: bufferPeriod, + logger: j.Logger, + tenantID: tenantID, + } + + j.auth = authInterface +} diff --git a/msgraph/msgraphintegration/constants.go b/msgraph/msgraphintegration/constants.go new file mode 100644 index 0000000..52320f2 --- /dev/null +++ b/msgraph/msgraphintegration/constants.go @@ -0,0 +1,12 @@ +// msgraph/msgraphintegration/constants.go +package msgraphintegration + +// Endpoint constants represent the URL suffixes used for Jamf API token interactions. +const ( + // Auth + oAuthTokenEndpoint string = "/oauth2/v2.0/token" + bearerTokenEndpoint string = "graph.microsoft.com" + invalidateTokenEndpoint string = "graph.microsoft.com" + oAuthTokenScope string = "https://graph.microsoft.com/.default" + baseAuthURL string = "https://login.microsoftonline.com" +) diff --git a/msgraph/msgraphintegration/header_exceptions.go b/msgraph/msgraphintegration/header_exceptions.go new file mode 100644 index 0000000..c7106c6 --- /dev/null +++ b/msgraph/msgraphintegration/header_exceptions.go @@ -0,0 +1,46 @@ +// msgraph/msgraphintegration/header_exceptions.go +package msgraphintegration + +import ( + _ "embed" + + "encoding/json" + "log" +) + +// EndpointConfig is a struct that holds configuration details for a specific API endpoint. +// It includes what type of content it can accept and what content type it should send. +type EndpointConfig struct { + Accept string `json:"accept"` // Accept specifies the MIME type the endpoint can handle in responses. + ContentType *string `json:"content_type"` // ContentType, if not nil, specifies the MIME type to set for requests sent to the endpoint. A pointer is used to distinguish between a missing field and an empty string. +} + +// ConfigMap is a map that associates endpoint URL patterns with their corresponding configurations. +// The map's keys are strings that identify the endpoint, and the values are EndpointConfig structs +// that hold the configuration for that endpoint. +type ConfigMap map[string]EndpointConfig + +// Variables +var configMap ConfigMap + +// Embedded Resources +// +//go:embed msgraph_api_exceptions_configuration.json +var graph_api_exceptions_configuration []byte + +// init is invoked automatically on package initialization and is responsible for +// setting up the default state of the package by loading the api exceptions configuration. +func init() { + // Load the default configuration from an embedded resource. + err := loadAPIExceptionsConfiguration() + if err != nil { + log.Fatalf("Error loading Microsoft Graph API exceptions configuration: %s", err) + } +} + +// loadAPIExceptionsConfiguration reads and unmarshals the graph_api_exceptions_configuration JSON data from an embedded file +// into the configMap variable, which holds the exceptions configuration for endpoint-specific headers. +func loadAPIExceptionsConfiguration() error { + // Unmarshal the embedded default configuration into the global configMap. + return json.Unmarshal(graph_api_exceptions_configuration, &configMap) +} diff --git a/msgraph/msgraphintegration/headers.go b/msgraph/msgraphintegration/headers.go new file mode 100644 index 0000000..3199470 --- /dev/null +++ b/msgraph/msgraphintegration/headers.go @@ -0,0 +1,63 @@ +// msgraph/msgraphintegration/headers.go +package msgraphintegration + +import ( + "strings" + + "go.uber.org/zap" +) + +// getContentTypeHeader determines the appropriate Content-Type header for a given API endpoint. +// It attempts to find a content type that matches the endpoint prefix in the global configMap. +// If a match is found and the content type is defined (not nil), it returns the specified content type. +// If the endpoint does not match any of the predefined patterns, "application/json" is used as a fallback. +// This method logs the decision process at various stages for debugging purposes. +func (m *Integration) getContentTypeHeader(endpoint string) string { + // Dynamic lookup from configuration should be the first priority + for key, config := range configMap { + if strings.HasPrefix(endpoint, key) { + if config.ContentType != nil { + m.Logger.Debug("Content-Type for endpoint found in configMap", zap.String("endpoint", endpoint), zap.String("content_type", *config.ContentType)) + return *config.ContentType + } + m.Logger.Debug("Content-Type for endpoint is nil in configMap, handling as special case", zap.String("endpoint", endpoint)) + // If a nil ContentType is an expected case, do not set Content-Type header. + return "" // Return empty to indicate no Content-Type should be set. + } + } + + // Fallback to JSON if no other match is found. + m.Logger.Debug("Content-Type for endpoint not found in configMap or standard patterns, using default JSON", zap.String("endpoint", endpoint)) + return "application/json" +} + +// GetAcceptHeader constructs and returns a weighted Accept header string for HTTP requests. +// The Accept header indicates the MIME types that the client can process and prioritizes them +// based on the quality factor (q) parameter. Higher q-values signal greater preference. +// This function specifies a range of MIME types with their respective weights, ensuring that +// the server is informed of the client's versatile content handling capabilities while +// indicating a preference for XML. The specified MIME types cover common content formats like +// images, JSON, XML, HTML, plain text, and certificates, with a fallback option for all other types. +func (m *Integration) getAcceptHeader() string { + weightedAcceptHeader := "application/x-x509-ca-cert;q=0.95," + + "application/pkix-cert;q=0.94," + + "application/pem-certificate-chain;q=0.93," + + "application/octet-stream;q=0.8," + // For general binary files + "image/png;q=0.75," + + "image/jpeg;q=0.74," + + "image/*;q=0.7," + + "application/xml;q=0.65," + + "text/xml;q=0.64," + + "text/xml;charset=UTF-8;q=0.63," + + "application/json;q=0.5," + + "text/html;q=0.5," + + "text/plain;q=0.4," + + "*/*;q=0.05" // Fallback for any other types + + return weightedAcceptHeader +} + +// getUserAgentHeader returns the User-Agent header string for the Microsoft Graph API. +func (m *Integration) getUserAgentHeader() string { + return "go-api-http-client-msgraph-integration" +} diff --git a/msgraph/msgraphintegration/interface.go b/msgraph/msgraphintegration/interface.go new file mode 100644 index 0000000..a4a246f --- /dev/null +++ b/msgraph/msgraphintegration/interface.go @@ -0,0 +1,52 @@ +// msgraph/msgraphintegration/interface.go +package msgraphintegration + +import ( + "net/http" + + "github.com/deploymenttheory/go-api-http-client/logger" +) + +// Integration implements the APIHandler interface for the Microsoft Graph API. +type Integration struct { + TenantID string // TenantID used for constructing the authentication endpoint. + TenantName string // TenantName used for constructing the authentication endpoint. + AuthMethodDescriptor string + Logger logger.Logger + auth authInterface +} + +// Info + +// Return the FQDN for Microsoft Graph +func (m *Integration) GetFQDN() string { + return m.getFQDN +} + +// TODO migrate strings +func (m *Integration) GetAuthMethodDescriptor() string { + return m.AuthMethodDescriptor +} + +// Utilities + +// TODO migrate strings +func (m *Integration) CheckRefreshToken() error { + return m.checkRefreshToken() +} + +// TODO migrate strings +func (m *Integration) PrepRequestParamsAndAuth(req *http.Request) error { + err := m.prepRequest(req) + return err +} + +// TODO migrate strings +func (m *Integration) PrepRequestBody(body interface{}, method string, endpoint string) ([]byte, error) { + return m.marshalRequest(body, method, endpoint) +} + +// TODO migrate strings +func (m *Integration) MarshalMultipartRequest(fields map[string]string, files map[string]string) ([]byte, string, error) { + return m.marshalMultipartRequest(fields, files) +} diff --git a/msgraph/msgraphintegration/marshall.go b/msgraph/msgraphintegration/marshall.go new file mode 100644 index 0000000..df89265 --- /dev/null +++ b/msgraph/msgraphintegration/marshall.go @@ -0,0 +1,88 @@ +// apiintegrations/msgraph/msgraph_api_request.go +package msgraphintegration + +import ( + "bytes" + "encoding/json" + "io" + "mime/multipart" + "path/filepath" + + "github.com/deploymenttheory/go-api-http-client-integrations/shared/helpers" + "go.uber.org/zap" +) + +// MarshalRequest encodes the request body as JSON for the Microsoft Graph API. +// This function takes an interface{} type body, an HTTP method, and an endpoint as input, +// and returns the marshaled JSON byte slice along with any error encountered during marshaling. +// The function ensures that the request body is always marshaled as JSON. +// It logs the JSON request body for POST, PUT, and PATCH methods using the integrated logger. +// +// Parameters: +// - body: The request body to be marshaled, of type interface{}. +// - method: The HTTP method being used for the request (e.g., "POST", "PUT", "PATCH"). +// - endpoint: The API endpoint for the request. +// +// Returns: +// - []byte: The marshaled JSON byte slice of the request body. +// - error: Any error encountered during the marshaling process. +// +// Logging: +// - Logs an error if JSON marshaling fails. +// - Logs the JSON request body for POST, PUT, and PATCH methods. +func (m *Integration) marshalRequest(body interface{}, method string, endpoint string) ([]byte, error) { + var ( + data []byte + err error + ) + + // Marshal the body as JSON + data, err = json.Marshal(body) + if err != nil { + m.Logger.Error("Failed marshaling JSON request", zap.Error(err)) + return nil, err + } + + // Log the JSON request body for POST, PUT, or PATCH methods + if method == "POST" || method == "PUT" || method == "PATCH" { + m.Logger.Debug("JSON Request Body", zap.String("Body", string(data))) + } + + return data, nil +} + +// MarshalMultipartRequest handles multipart form data encoding with secure file handling and returns the encoded body and content type. +func (m *Integration) marshalMultipartRequest(fields map[string]string, files map[string]string) ([]byte, string, error) { + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + for field, value := range fields { + if err := writer.WriteField(field, value); err != nil { + return nil, "", err + } + } + + for formField, filePath := range files { + file, err := helpers.SafeOpenFile(filePath) + if err != nil { + m.Logger.Error("Failed to open file securely", zap.String("file", filePath), zap.Error(err)) + return nil, "", err + } + defer file.Close() + + part, err := writer.CreateFormFile(formField, filepath.Base(filePath)) + if err != nil { + return nil, "", err + } + if _, err := io.Copy(part, file); err != nil { + return nil, "", err + } + } + + contentType := writer.FormDataContentType() + if err := writer.Close(); err != nil { + return nil, "", err + } + + return body.Bytes(), contentType, nil +} diff --git a/msgraph/msgraphintegration/msgraph_api_exceptions_configuration.json b/msgraph/msgraphintegration/msgraph_api_exceptions_configuration.json new file mode 100644 index 0000000..0db3279 --- /dev/null +++ b/msgraph/msgraphintegration/msgraph_api_exceptions_configuration.json @@ -0,0 +1,3 @@ +{ + +} diff --git a/msgraph/msgraphintegration/request.go b/msgraph/msgraphintegration/request.go new file mode 100644 index 0000000..e4697cf --- /dev/null +++ b/msgraph/msgraphintegration/request.go @@ -0,0 +1,38 @@ +// apiintegrations/msgraph/request.go +package msgraphintegration + +import ( + "fmt" + "net/http" +) + +// prepRequest prepares an HTTP request by setting the necessary headers and handling authorization. +// This function adds headers for Accept, Content-Type, User-Agent, and Authorization based on the Integration's methods +// and checks for token refresh if needed. +// +// Parameters: +// - req: A pointer to the http.Request that needs to be prepared. +// +// Returns: +// - error: Any error encountered while checking the refresh token or setting headers. Returns nil if no errors occur. +// +// Functionality: +// - Adds an "Accept" header based on the Integration's getAcceptHeader method. +// - Adds a "Content-Type" header based on the Integration's getContentTypeHeader method, which depends on the request URL. +// - Adds a "User-Agent" header based on the Integration's getUserAgentHeader method. +// - Checks and refreshes the token if necessary using the Integration's checkRefreshToken method. +// - Adds an "Authorization" header with a Bearer token obtained from the Integration's auth.getTokenString method. +func (m *Integration) prepRequest(req *http.Request) error { + req.Header.Add("Accept", m.getAcceptHeader()) + req.Header.Add("Content-Type", m.getContentTypeHeader(req.URL.String())) + req.Header.Add("User-Agent", m.getUserAgentHeader()) + + err := m.checkRefreshToken() + if err != nil { + return err + } + + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", m.auth.getTokenString())) + + return nil +} diff --git a/msgraph/msgraphintegration/urls.go b/msgraph/msgraphintegration/urls.go new file mode 100644 index 0000000..3f87b52 --- /dev/null +++ b/msgraph/msgraphintegration/urls.go @@ -0,0 +1,12 @@ +// apiintegrations/msgraph/urls.go +package msgraphintegration + +// GetTenantID returns the tenant ID for the Microsoft Graph integration. +func (m *Integration) GetTenantID() string { + return m.TenantID +} + +// getFQDN returns the fully qualified domain name for Microsoft Graph. +func (m *Integration) getFQDN() string { + return "https://graph.microsoft.com" +} diff --git a/jamfpro/jamfprointegration/helpers.go b/shared/helpers/helpers.go similarity index 96% rename from jamfpro/jamfprointegration/helpers.go rename to shared/helpers/helpers.go index 84b7d94..d86fbe2 100644 --- a/jamfpro/jamfprointegration/helpers.go +++ b/shared/helpers/helpers.go @@ -1,4 +1,4 @@ -package jamfprointegration +package helpers import ( "fmt"