Created
August 23, 2024 20:13
-
-
Save Krovikan-Vamp/2a0b9b5bb1ec8ebfcae9507dad7ec1b5 to your computer and use it in GitHub Desktop.
My First Go Package
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
package msgraph | |
import ( | |
"bytes" | |
"context" | |
"encoding/json" | |
"fmt" | |
"io" | |
"net/http" | |
"os" | |
"sync" | |
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential" | |
"github.com/Kobargo/UKG-Custodian/logger" | |
) | |
func InitGraphClient(creds GraphClientCredentials) *GraphApi { | |
log := logger.Logger | |
log.Debug().Discard().Msgf("Initializing Microsoft Graph API client with credentials: %s", creds) | |
if creds.ClientSecret == "" { | |
env_secret := os.Getenv("client_secret") | |
if env_secret == "" { | |
log.Error().Msgf("graphapi.InitGraphClient: Failed to obtain a valid secret for credentials. Reading: \"%s\"", env_secret) | |
} | |
creds.ClientSecret = env_secret | |
} | |
generatedCredential, err := confidential.NewCredFromSecret(creds.ClientSecret) | |
if err != nil { | |
log.Err(err).Msgf("main: could not create credential: %s", err) | |
} | |
confidentialClient, err := confidential.New(fmt.Sprintf("https://login.microsoftonline.com/%s", creds.TenantId), creds.ClientId, generatedCredential) | |
if err != nil { | |
log.Err(err).Msgf("graphapi.InitGraphClient: Failed to create confidential client: %s", err) | |
} | |
scopes := []string{".default"} | |
result, err := confidentialClient.AcquireTokenSilent(context.TODO(), scopes) | |
if err != nil { | |
// cache miss, authenticate with another AcquireToken... method | |
result, err = confidentialClient.AcquireTokenByCredential(context.TODO(), scopes) | |
if err != nil { | |
// TODO: handle error | |
log.Err(err).Msgf("graphapi.InitGraphClient: failed to acquire access token: %s", err) | |
} | |
} | |
return &GraphApi{ | |
TenantName: creds.TenantName, | |
SessionToken: result.AccessToken, | |
ApiURL: "https://graph.microsoft.com/v1.0", | |
Logger: logger.Logger, | |
Session: &http.Client{}, | |
} | |
} | |
func (api *GraphApi) NewRequest(method string, uri string, body io.Reader) (*http.Request, error) { | |
req, err := http.NewRequest(method, uri, body) | |
if err != nil { | |
api.Logger.Err(err).Msgf("graphapi.NewRequest: Failed to build %s \"%s\" request. %s", method, uri, err) | |
} | |
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", api.SessionToken)) | |
req.Header.Add("Content-Type", "application/json") | |
req.Header.Add("ConsistencyLevel", "eventual") | |
return req, nil | |
} | |
func (api *GraphApi) QueryUser(upn string) (UserAccount, error) { | |
if upn == "" { | |
api.Logger.Warn().Str("tip", "Check the UPN you are querying is not nil.").Str("hint", "If you want all users, use GraphApi.GetUsers() method.").Msg("Cannot search for nil UPN.") | |
return UserAccount{}, fmt.Errorf("Cannot search for nil UPN.") | |
} | |
uri := fmt.Sprintf("%s/users/%s", api.ApiURL, upn) | |
req, err := api.NewRequest("GET", uri, nil) | |
if err != nil { | |
api.Logger.Err(err).Msg("graphapi.QueryUser: failed to build request") | |
} | |
res, err := api.Session.Do(req) | |
if err != nil { | |
api.Logger.Err(err).Msg("graphapi.QueryUser: failed to get a response from API") | |
} | |
defer res.Body.Close() | |
if res.StatusCode == http.StatusNotFound { | |
return UserAccount{}, fmt.Errorf("User not found with UPN: %s", upn) | |
} | |
body, err := io.ReadAll(res.Body) | |
if err != nil { | |
api.Logger.Err(err).Msg("graphapi.QueryUser: failed to read response body") | |
} | |
var userAccount UserAccount | |
err = json.Unmarshal(body, &userAccount) | |
if err != nil { | |
api.Logger.Err(err).Msg("could not unmarshal userAccount") | |
} | |
return userAccount, nil | |
} | |
func (api *GraphApi) CreateUser(userInfo UserAccount) (UserAccount, error) { | |
marshaledData, err := json.Marshal(userInfo) | |
if err != nil { | |
api.Logger.Err(err).Msg("could not marshal the provided parameter") | |
} | |
uri := fmt.Sprintf("%s/users", api.ApiURL) | |
// api.Logger.Debug().Msg(fmt.Sprintf("Marshaled User: %v", string(marshaledData))) | |
req, err := api.NewRequest("POST", uri, bytes.NewBuffer(marshaledData)) | |
if err != nil { | |
api.Logger.Err(err).Msg("Failed to construct NewRequest") | |
} | |
res, err := api.Session.Do(req) | |
if err != nil { | |
api.Logger.Err(err).Msg("Failed to get response from user create request.") | |
} | |
defer res.Body.Close() | |
body, err := io.ReadAll(res.Body) | |
if err != nil { | |
api.Logger.Err(err).Msg("graphapi.QueryUser: failed to read response body") | |
} | |
// Handle response statuscode cases | |
switch res.StatusCode { | |
case 400: | |
var graphError GraphAPIErrorResponse | |
err = json.Unmarshal(body, &graphError) | |
if err != nil { | |
api.Logger.Err(err).Msg("Failed to unmarshal the ERROR response.") | |
} | |
if graphError.Error.Message == "Another object with the same value for property userPrincipalName already exists." { | |
api.Logger.Warn().Str("tip", "User already exists").Msgf("You tried to create %s but it already exists!", userInfo.UserPrincipalName) | |
return api.QueryUser(userInfo.UserPrincipalName) | |
} | |
// api.Logger.Debug().Int("statusCode", res.StatusCode).Str("response", string(body)).Str("marshaledData", fmt.Sprintf("%v", graphError)).Send() | |
return UserAccount{}, fmt.Errorf("%v: %s", graphError.Error.Code, graphError.Error.Message) | |
case 201: | |
var createdAccount UserAccount | |
err = json.Unmarshal(body, &createdAccount) | |
if err != nil { | |
api.Logger.Err(err).Msg("Failed to unmarshal the response.") | |
} | |
return createdAccount, nil | |
default: | |
return UserAccount{}, fmt.Errorf("Received an unexpected error code %v", res.StatusCode) | |
} | |
} | |
func (api *GraphApi) UpdateUser(userInfo UserAccount) (UserAccount, error) { | |
marshaledData, err := json.Marshal(userInfo) | |
if err != nil { | |
api.Logger.Err(err).Msg("could not marshal the provided parameter") | |
} | |
api.Logger.Debug().Msgf("%+v", string(marshaledData)) | |
uri := fmt.Sprintf("%s/users/%s", api.ApiURL, userInfo.UserPrincipalName) | |
// api.Logger.Debug().Msg(fmt.Sprintf("Marshaled User: %v", string(marshaledData))) | |
req, err := api.NewRequest("PATCH", uri, bytes.NewBuffer(marshaledData)) | |
if err != nil { | |
api.Logger.Err(err).Msg("Failed to construct NewRequest") | |
} | |
res, err := api.Session.Do(req) | |
if err != nil { | |
api.Logger.Err(err).Msg("Failed to get response from user create request.") | |
} | |
defer res.Body.Close() | |
body, err := io.ReadAll(res.Body) | |
if err != nil { | |
api.Logger.Err(err).Msg("graphapi.QueryUser: failed to read response body") | |
} | |
// Handle response statuscode cases | |
switch res.StatusCode { | |
case http.StatusBadRequest: | |
var graphError GraphAPIErrorResponse | |
err = json.Unmarshal(body, &graphError) | |
if err != nil { | |
api.Logger.Err(err).Msg("Failed to unmarshal the ERROR response.") | |
} | |
api.Logger.Debug().Int("statusCode", res.StatusCode).Str("response", string(body)).Str("marshaledData", fmt.Sprintf("%v", graphError)).Send() | |
return UserAccount{}, fmt.Errorf("%v: %s", graphError.Error.Code, graphError.Error.Message) | |
case http.StatusNotFound: | |
var graphError GraphAPIErrorResponse | |
err = json.Unmarshal(body, &graphError) | |
if err != nil { | |
api.Logger.Err(err).Msg("Failed to unmarshal the ERROR response.") | |
} | |
if graphError.Error.Code == "Request_ResourceNotFound" { | |
api.Logger.Error().Str("tip", "User does not exist").Msgf("You tried to modify %s but it does not exist!", userInfo.UserPrincipalName) | |
return UserAccount{}, fmt.Errorf("You tried to modify %s but it does not exist!", userInfo.UserPrincipalName) | |
} | |
api.Logger.Debug().Int("statusCode", res.StatusCode).Str("response", string(body)).Str("marshaledData", fmt.Sprintf("%v", graphError)).Send() | |
return UserAccount{}, fmt.Errorf("%v: %s", graphError.Error.Code, graphError.Error.Message) | |
case http.StatusNoContent: | |
api.Logger.Info().Str("tip", "You may want to sleep for a few seconds after updating a user's properties.").Msgf("Successfully updated %s", userInfo.UserPrincipalName) | |
return api.QueryUser(userInfo.UserPrincipalName) | |
default: | |
var graphError GraphAPIErrorResponse | |
err = json.Unmarshal(body, &graphError) | |
if err != nil { | |
api.Logger.Err(err).Msg("Failed to unmarshal the ERROR response.") | |
} | |
if graphError.Error.Message == "Insufficient privileges to complete the operation." { | |
api.Logger.Warn().Str("tip", "You may be trying to set a property that isn't allowed.").Msgf("You tried to modify %s but you were unauthorized!", userInfo.UserPrincipalName) | |
return UserAccount{}, fmt.Errorf("Unable to update account %v %+v", res.StatusCode, string(body)) | |
} | |
return UserAccount{}, fmt.Errorf("Received an unexpected error code %v %+v", res.StatusCode, string(body)) | |
} | |
} | |
func (api *GraphApi) GetUsersGroups(userId string) ([]Group, error) { | |
if userId == "" { | |
api.Logger.Warn().Str("tip", "Check the userId you are querying is not nil.").Str("hint", "If you want all users, use GraphApi.GetUsers() method.").Msg("Cannot search for nil UPN.") | |
return []Group{}, fmt.Errorf("Cannot search for nil userId.") | |
} | |
uri := fmt.Sprintf("%s/users/%s/memberOf", api.ApiURL, userId) | |
req, err := api.NewRequest("GET", uri, nil) | |
if err != nil { | |
api.Logger.Err(err).Msg("Failed to build request") | |
} | |
res, err := api.Session.Do(req) | |
if err != nil { | |
api.Logger.Err(err).Msg("Failed to get a response from API") | |
} | |
defer res.Body.Close() | |
switch res.StatusCode { | |
case http.StatusNotFound: | |
return []Group{}, fmt.Errorf("User not found with userId: %s", userId) | |
case http.StatusOK: | |
body, err := io.ReadAll(res.Body) | |
if err != nil { | |
api.Logger.Err(err).Msg("Failed to read response body") | |
} | |
var memberships GroupMemberships | |
err = json.Unmarshal(body, &memberships) | |
if err != nil { | |
api.Logger.Err(err).Msg("Failed to unmarshal the response") | |
} | |
return memberships.Value, nil | |
default: | |
return []Group{}, fmt.Errorf("Received unexpected status code %v", res.StatusCode) | |
} | |
} | |
func (api *GraphApi) AssignUserToGroups(userId string, groupIds []string) ([]string, []GraphAPIError, error) { | |
if userId == "" { | |
return []string{}, []GraphAPIError{}, fmt.Errorf("Invalid userId parameter. Reading: '%v'", userId) | |
} | |
var wg sync.WaitGroup | |
var mutex sync.Mutex | |
var addedGroups []string | |
var failedGroups []GraphAPIError | |
for _, groupId := range groupIds { | |
wg.Add(1) | |
go func(groupId string) { | |
defer wg.Done() | |
uri := fmt.Sprintf("%s/groups/%s/members/$ref", api.ApiURL, groupId) | |
body := GroupAppend{ | |
ODataID: fmt.Sprintf("%s/users/%s", api.ApiURL, userId), | |
} | |
api.Logger.Debug().Msgf("Constructed URI: %s", uri) | |
api.Logger.Debug().Msgf("Constructed request body: %+v", body) | |
marshaledRequestBody, err := json.Marshal(body) | |
if err != nil { | |
api.Logger.Err(err).Str("assignmentFailed", groupId).Msg("Failed to marshal the request body") | |
} | |
api.Logger.Debug().Msgf("Marshaled request body: %v", marshaledRequestBody) | |
req, err := api.NewRequest(http.MethodPost, uri, bytes.NewBuffer(marshaledRequestBody)) | |
if err != nil { | |
api.Logger.Err(err).Msg("Failed to construct the requst") | |
} | |
res, err := api.Session.Do(req) | |
if err != nil { | |
api.Logger.Err(err).Msg("Failed to get response.") | |
} | |
defer res.Body.Close() | |
switch res.StatusCode { | |
case http.StatusNoContent: | |
mutex.Lock() | |
addedGroups = append(addedGroups, groupId) | |
mutex.Unlock() | |
api.Logger.Info().Msgf("%s was added to %s", userId, groupId) | |
case http.StatusBadRequest: | |
body, err := io.ReadAll(res.Body) // extract the error message so we can view it later | |
if err != nil { | |
api.Logger.Err(err).Msg("Failed to read response body") | |
} | |
var graphError GraphAPIErrorResponse | |
err = json.Unmarshal(body, &graphError) | |
if err != nil { | |
api.Logger.Err(err).Msg("Failed to unmarshal the ERROR response.") | |
} | |
mutex.Lock() | |
failedGroups = append(failedGroups, graphError.Error) | |
mutex.Unlock() | |
default: | |
api.Logger.Error().Msgf("Received unexpected status code \"%v\" while adding to group: %s", res.StatusCode, groupId) | |
} | |
}(groupId) | |
} | |
wg.Wait() | |
return addedGroups, failedGroups, 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
// logger/logger.go | |
package logger | |
import ( | |
"os" | |
"strings" | |
"github.com/rs/zerolog" | |
"github.com/rs/zerolog/log" | |
) | |
var Logger zerolog.Logger | |
func init() { | |
// Initialize the logger with your desired configuration | |
Logger = log.Output(zerolog.ConsoleWriter{ | |
Out: os.Stdout, | |
NoColor: false, | |
TimeFormat: "2006-01-02 15:04:05", | |
FormatLevel: func(i interface{}) string { | |
if ll, ok := i.(string); ok { | |
switch ll { | |
case "debug": | |
return "\033[35m" + strings.ToUpper(ll) + "\033[0m" | |
case "info": | |
return "\033[36m" + strings.ToUpper(ll) + "\033[0m " | |
case "warn": | |
return "\033[33m" + strings.ToUpper(ll) + "\033[0m " | |
case "error": | |
return "\033[31m" + strings.ToUpper(ll) + "\033[0m" | |
default: | |
return strings.ToUpper(ll) | |
} | |
} | |
return "" | |
}, | |
}).With().Caller().Logger() | |
} |
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
package msgraph | |
import ( | |
"net/http" | |
"github.com/rs/zerolog" | |
) | |
type GraphClientCredentials struct { | |
TenantName string | |
TenantId string | |
ClientSecret string | |
ClientId string | |
} | |
type GraphApi struct { | |
TenantName string | |
SessionToken string | |
ApiURL string | |
Logger zerolog.Logger | |
Session *http.Client | |
} | |
type PasswordProfile struct { | |
ForceChangePasswordNextSignIn bool `json:"forceChangePasswordNextSignIn,omitempty"` | |
Password string `json:"password,omitempty"` | |
} | |
type GraphAPIError struct { | |
Code string `json:"code"` | |
Message string `json:"message"` | |
} | |
type GraphAPIErrorResponse struct { | |
Error GraphAPIError `json:"error"` | |
} | |
type GroupMemberships struct { | |
ODContext string `json:"@odata.context"` | |
Count string `json:"@odata.count,omitempty"` | |
Value []Group `json:"value"` | |
} | |
type Group struct { | |
ID string `json:"id"` | |
DisplayName string `json:"displayName"` | |
// optional i think | |
Mail string `json:"mail,omitempty"` | |
MailNickname string `json:"mailNickname,omitempty"` | |
MailEnabled bool `json:"mailEnabled,omitempty"` | |
SecurityEnabled bool `json:"securityEnabled,omitempty"` | |
} | |
type GroupAppend struct { | |
ODataID string `json:"@odata.id,empty"` | |
} | |
// GENERATED | |
type UserAccount struct { | |
ID string `json:"id"` | |
AccountEnabled bool `json:"accountEnabled"` | |
UserPrincipalName string `json:"userPrincipalName"` | |
DisplayName string `json:"displayName"` | |
BusinessPhones []string `json:"businessPhones,omitempty"` | |
GivenName string `json:"givenName,omitempty"` | |
JobTitle string `json:"jobTitle"` | |
Mail string `json:"mail"` | |
MailNickname string `json:"mailNickname"` | |
MobilePhone string `json:"mobilePhone,omitempty"` | |
OfficeLocation string `json:"officeLocation,omitempty"` | |
PreferredLanguage string `json:"preferredLanguage,omitempty"` | |
Surname string `json:"surname,omitempty"` | |
PasswordProfile PasswordProfile `json:"passwordProfile,omitempty"` | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment