Skip to content

Instantly share code, notes, and snippets.

@Krovikan-Vamp
Created August 23, 2024 20:13
Show Gist options
  • Save Krovikan-Vamp/2a0b9b5bb1ec8ebfcae9507dad7ec1b5 to your computer and use it in GitHub Desktop.
Save Krovikan-Vamp/2a0b9b5bb1ec8ebfcae9507dad7ec1b5 to your computer and use it in GitHub Desktop.
My First Go Package
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
}
// 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()
}
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