4f3f07d4dd
- Vendor shuffle-shared v1.2.51 as backend/go-app/shuffle-shared - Add replace directive in go.mod to use the local moduled copy - In HandleCheckLicense, force org.Licensed=true and set every SyncFeatures limit to 1e9, skipping all license-key logic - Update Dockerfile to ADD the local shuffle-shared before go build - Verified: backend image builds successfully via docker
2051 lines
61 KiB
Go
2051 lines
61 KiB
Go
package shuffle
|
|
|
|
// Shuffle is an automation platform for security and IT. This app and the associated scopes enables us to get information about a user, their mailbox and eventually subscribing them to send pub/sub requests to our platform to handle their emails in real-time, before controlling how to handle the data themselves.
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"log"
|
|
"net/http"
|
|
"regexp"
|
|
"strconv"
|
|
|
|
//"net/url"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/go-querystring/query"
|
|
"golang.org/x/oauth2"
|
|
|
|
"path/filepath"
|
|
|
|
corev1 "k8s.io/api/core/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/client-go/kubernetes"
|
|
"k8s.io/client-go/rest"
|
|
"k8s.io/client-go/tools/clientcmd"
|
|
"k8s.io/client-go/util/homedir"
|
|
)
|
|
|
|
var handledIds []string
|
|
|
|
/*
|
|
func fetchUserInfoFromToken(ctx context.Context, accessToken string, issuer string, openIdAuthUrl string) (map[string]interface{}, error) {
|
|
// Get well-known config to find userinfo endpoint
|
|
config, err := fetchWellKnownConfig(ctx, issuer, openIdAuthUrl)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get OIDC config: %w", err)
|
|
}
|
|
|
|
// Get userinfo endpoint
|
|
userinfoEndpoint, ok := config["userinfo_endpoint"].(string)
|
|
if !ok {
|
|
return nil, fmt.Errorf("no userinfo_endpoint in OIDC config")
|
|
}
|
|
|
|
// Handle Microsoft Azure AD userinfo endpoint issues
|
|
if strings.Contains(userinfoEndpoint, "login.microsoftonline.com") {
|
|
userinfoEndpoint = "https://graph.microsoft.com/v1.0/me"
|
|
log.Printf("Using Microsoft Graph /me endpoint instead of: %s", userinfoEndpoint)
|
|
}
|
|
|
|
// Call userinfo/me endpoint with access token
|
|
req, err := http.NewRequest("GET", userinfoEndpoint, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create userinfo request: %w", err)
|
|
}
|
|
|
|
if len(accessToken) == 0 {
|
|
return nil, fmt.Errorf("access token is empty")
|
|
}
|
|
|
|
req.Header.Set("Authorization", "Bearer "+accessToken)
|
|
req.Header.Set("Accept", "application/json")
|
|
|
|
client := &http.Client{}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to call userinfo endpoint: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != 200 {
|
|
body, _ := ioutil.ReadAll(resp.Body)
|
|
return nil, fmt.Errorf("userinfo endpoint returned status %d: %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
// Parse userinfo response
|
|
var userInfo map[string]interface{}
|
|
if err := json.NewDecoder(resp.Body).Decode(&userInfo); err != nil {
|
|
return nil, fmt.Errorf("failed to decode userinfo response: %w", err)
|
|
}
|
|
|
|
// Normalize Microsoft Graph fields to standard OIDC fields
|
|
if mail, ok := userInfo["mail"].(string); ok && userInfo["email"] == nil {
|
|
userInfo["email"] = mail
|
|
}
|
|
if displayName, ok := userInfo["displayName"].(string); ok && userInfo["name"] == nil {
|
|
userInfo["name"] = displayName
|
|
}
|
|
if id, ok := userInfo["id"].(string); ok && userInfo["sub"] == nil {
|
|
userInfo["sub"] = id
|
|
}
|
|
|
|
return userInfo, nil
|
|
}
|
|
*/
|
|
|
|
func GetOutlookAttachmentList(client *http.Client, emailId string) (MailDataOutlookList, error) {
|
|
requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/me/messages/%s/attachments", emailId)
|
|
//log.Printf("Outlook email URL: %#v", requestUrl)
|
|
|
|
ret, err := client.Get(requestUrl)
|
|
if err != nil {
|
|
log.Printf("[INFO] OutlookErr: %s", err)
|
|
return MailDataOutlookList{}, err
|
|
}
|
|
|
|
body, err := ioutil.ReadAll(ret.Body)
|
|
if err != nil {
|
|
log.Printf("[WARNING] Failed body decoding from outlook email")
|
|
return MailDataOutlookList{}, err
|
|
}
|
|
|
|
//type FullEmail struct {
|
|
//log.Printf("[INFO] Attachment List Body: %s", string(body))
|
|
//log.Printf("[INFO] Status email: %d", ret.StatusCode)
|
|
if ret.StatusCode != 200 {
|
|
return MailDataOutlookList{}, err
|
|
}
|
|
|
|
var list MailDataOutlookList
|
|
err = json.Unmarshal(body, &list)
|
|
if err != nil {
|
|
log.Printf("[INFO] Email unmarshal error: %s", err)
|
|
return MailDataOutlookList{}, err
|
|
}
|
|
|
|
return list, nil
|
|
}
|
|
|
|
func GetOutlookAttachment(client *http.Client, emailId, attachmentId string) (OutlookAttachment, []byte, error) {
|
|
//requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/users/ec03b4f2-fccf-4c35-b0eb-be85a0f5dd43/mailFolders")
|
|
|
|
requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/me/messages/%s/attachments/%s", emailId, attachmentId)
|
|
//log.Printf("Outlook email URL: %#v", requestUrl)
|
|
body := []byte{}
|
|
|
|
ret, err := client.Get(requestUrl)
|
|
if err != nil {
|
|
log.Printf("[INFO] OutlookErr: %s", err)
|
|
return OutlookAttachment{}, body, err
|
|
}
|
|
|
|
body, err = ioutil.ReadAll(ret.Body)
|
|
if err != nil {
|
|
log.Printf("[WARNING] Failed body decoding from outlook email")
|
|
return OutlookAttachment{}, body, err
|
|
}
|
|
|
|
//type FullEmail struct {
|
|
//log.Printf("[INFO] Attachment Body (1): %s", string(body))
|
|
//log.Printf("[INFO] Status email (1): %d", ret.StatusCode)
|
|
if ret.StatusCode != 200 {
|
|
return OutlookAttachment{}, body, err
|
|
}
|
|
|
|
// Gets the data
|
|
var attachment OutlookAttachment
|
|
err = json.Unmarshal(body, &attachment)
|
|
if err != nil {
|
|
log.Printf("[INFO] Email unmarshal error: %s", err)
|
|
return OutlookAttachment{}, body, err
|
|
}
|
|
|
|
requestUrl = fmt.Sprintf("https://graph.microsoft.com/v1.0/me/messages/%s/attachments/%s/$value", emailId, attachmentId)
|
|
//log.Printf("Outlook email URL: %#v", requestUrl)
|
|
|
|
ret, err = client.Get(requestUrl)
|
|
if err != nil {
|
|
log.Printf("[INFO] OutlookErr: %s", err)
|
|
return OutlookAttachment{}, body, err
|
|
}
|
|
|
|
body, err = ioutil.ReadAll(ret.Body)
|
|
if err != nil {
|
|
log.Printf("[WARNING] Failed body decoding from outlook email")
|
|
return OutlookAttachment{}, body, err
|
|
}
|
|
|
|
//type FullEmail struct {
|
|
//log.Printf("[INFO] Attachment Body (2): %s", string(body))
|
|
//log.Printf("[INFO] Status email (2): %d", ret.StatusCode)
|
|
if ret.StatusCode != 200 {
|
|
return OutlookAttachment{}, body, err
|
|
}
|
|
|
|
return attachment, body, nil
|
|
}
|
|
|
|
func GetOutlookEmail(client *http.Client, maildata MailDataOutlook) ([]FullEmail, error) {
|
|
//requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/users/ec03b4f2-fccf-4c35-b0eb-be85a0f5dd43/mailFolders")
|
|
|
|
emails := []FullEmail{}
|
|
for _, email := range maildata.Value {
|
|
//messageId := email.Resourcedata.ID
|
|
//requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/me/%s", messageId)
|
|
requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/%s", email.Resource)
|
|
//log.Printf("Outlook email URL: %#v", requestUrl)
|
|
|
|
ret, err := client.Get(requestUrl)
|
|
if err != nil {
|
|
log.Printf("[INFO] OutlookErr: %s", err)
|
|
return []FullEmail{}, err
|
|
}
|
|
|
|
body, err := ioutil.ReadAll(ret.Body)
|
|
if err != nil {
|
|
log.Printf("[WARNING] Failed body decoding from outlook email")
|
|
return []FullEmail{}, err
|
|
}
|
|
|
|
//type FullEmail struct {
|
|
//log.Printf("[INFO] EMAIL Body: %s", string(body))
|
|
//log.Printf("[INFO] Status email: %d", ret.StatusCode)
|
|
if ret.StatusCode != 200 {
|
|
return []FullEmail{}, err
|
|
}
|
|
|
|
//log.Printf("Body: %s", string(body))
|
|
|
|
parsedmail := FullEmail{}
|
|
err = json.Unmarshal(body, &parsedmail)
|
|
if err != nil {
|
|
log.Printf("[INFO] Email unmarshal error: %s", err)
|
|
return []FullEmail{}, err
|
|
}
|
|
|
|
emails = append(emails, parsedmail)
|
|
}
|
|
|
|
return emails, nil
|
|
}
|
|
|
|
// FIXME:
|
|
// 1. Should find contributions to Shuffle repo's for the user
|
|
// 2. Should save tokens to continuously check this
|
|
func HandleNewGithubRegister(resp http.ResponseWriter, request *http.Request) {
|
|
cors := HandleCors(resp, request)
|
|
if cors {
|
|
return
|
|
}
|
|
|
|
user, err := HandleApiAuthentication(resp, request)
|
|
if err != nil {
|
|
log.Printf("[INFO] Api authentication failed in setting gmail: %s", err)
|
|
resp.WriteHeader(401)
|
|
resp.Write([]byte(`{"success": false}`))
|
|
return
|
|
}
|
|
|
|
body, err := ioutil.ReadAll(request.Body)
|
|
if err != nil {
|
|
log.Printf("[WARNING] Error with body read in github auth: %s", err)
|
|
resp.WriteHeader(401)
|
|
resp.Write([]byte(`{"success": false}`))
|
|
return
|
|
}
|
|
|
|
log.Printf("BODY: %s", string(body))
|
|
type GithubAuth struct {
|
|
User string `json:"user"`
|
|
Type string `json:"github"`
|
|
Code string `json:"code"`
|
|
}
|
|
|
|
var authInfo GithubAuth
|
|
err = json.Unmarshal(body, &authInfo)
|
|
if err != nil {
|
|
log.Printf("[WARNING] Failed unmarshaling (githubauth): %s", err)
|
|
resp.WriteHeader(401)
|
|
resp.Write([]byte(`{"success": false, "reason": "Bad data received"}`))
|
|
return
|
|
}
|
|
|
|
if authInfo.User != user.Id {
|
|
log.Printf("[WARNING] Bad user - not matching with auth: %s", err)
|
|
resp.WriteHeader(401)
|
|
resp.Write([]byte(`{"success": false, "reason": "Bad user ID - not matching"}`))
|
|
return
|
|
}
|
|
|
|
ctx := GetContext(request)
|
|
url := fmt.Sprintf("http://%s%s/set_authentication", request.Host, request.URL.EscapedPath())
|
|
if project.Environment == "cloud" && os.Getenv("CLOUD_ENVIRONMENT") != "local" {
|
|
url = fmt.Sprintf("https://%s%s/set_authentication", request.Host, request.URL.EscapedPath())
|
|
}
|
|
|
|
log.Printf("URI: %s", url)
|
|
|
|
client, accessToken, err := GetGithubClient(ctx, authInfo.Code, OauthToken{}, url)
|
|
if err != nil {
|
|
log.Printf("[WARNING] Failed setting up github client for %s (%s): %s", user.Username, user.Id, err)
|
|
resp.WriteHeader(401)
|
|
resp.Write([]byte(`{"success": false}`))
|
|
return
|
|
}
|
|
|
|
ghuser, err := GetGithubProfile(ctx, client)
|
|
if err != nil {
|
|
log.Printf("[WARNING] Failed setting github profile for %s (%s): %s", user.Username, user.Id, err)
|
|
resp.WriteHeader(401)
|
|
resp.Write([]byte(`{"success": false}`))
|
|
return
|
|
}
|
|
|
|
user.PublicProfile.Public = true
|
|
user.PublicProfile.GithubUsername = ghuser.Login
|
|
user.PublicProfile.GithubUserid = strconv.Itoa(ghuser.ID)
|
|
user.PublicProfile.GithubAvatar = ghuser.AvatarURL
|
|
|
|
if len(user.PublicProfile.GithubAvatar) == 0 {
|
|
user.PublicProfile.GithubAvatar = ghuser.AvatarURL
|
|
}
|
|
|
|
user.PublicProfile.GithubLocation = ghuser.Location
|
|
user.PublicProfile.GithubUrl = ghuser.Blog
|
|
user.PublicProfile.GithubBio = ghuser.Bio
|
|
user.PublicProfile.GithubTwitter = ghuser.TwitterUsername
|
|
|
|
//GET /repos/{owner}/{repo}/contributors
|
|
repositories := map[string]string{
|
|
"frikky/shuffle": "core",
|
|
"frikky/shuffle-shared": "core",
|
|
"shuffle/shuffle-docs": "docs",
|
|
"shuffle/shuffle-apps": "apps",
|
|
"shuffle/openapi-apps": "apps",
|
|
"shuffle/shuffle-usecases": "workflows",
|
|
}
|
|
|
|
// Reset
|
|
user.PublicProfile.GithubContributions = GithubContributions{}
|
|
for repo, repoType := range repositories {
|
|
contributors, err := GetGithubRepoContributors(ctx, client, repo)
|
|
if err != nil {
|
|
log.Printf("[ERROR] Failed getting user repo contributions for %s", user.Username)
|
|
continue
|
|
}
|
|
|
|
for _, contributor := range contributors {
|
|
if contributor.Login == user.PublicProfile.GithubUsername {
|
|
log.Printf("Contrib! Repo: %s, user: %s, contributions: %d", repo, contributor.Login, contributor.Contributions)
|
|
|
|
if repoType == "core" {
|
|
user.PublicProfile.GithubContributions.Core.Count += contributor.Contributions
|
|
} else if repoType == "docs" {
|
|
user.PublicProfile.GithubContributions.Docs.Count += contributor.Contributions
|
|
|
|
} else if repoType == "apps" {
|
|
user.PublicProfile.GithubContributions.Apps.Count += contributor.Contributions
|
|
|
|
} else if repoType == "workflows" {
|
|
user.PublicProfile.GithubContributions.Workflows.Count += contributor.Contributions
|
|
|
|
} else {
|
|
log.Printf("[WARNING] No handler for repotype %s (%s)", repoType, repo)
|
|
}
|
|
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
log.Printf("CONTRIB: %#v", user.PublicProfile.GithubContributions)
|
|
|
|
err = SetUser(ctx, &user, false)
|
|
if err != nil {
|
|
log.Printf("[WARNING] Failed setting user data for %s: %s (github)", user.Username, err)
|
|
resp.WriteHeader(401)
|
|
return
|
|
}
|
|
|
|
trigger := TriggerAuth{}
|
|
trigger.Id = fmt.Sprintf("github_%s", user.Id)
|
|
trigger.Username = fmt.Sprintf("%s", user.Username)
|
|
trigger.OrgId = user.ActiveOrg.Id
|
|
trigger.Owner = user.Id
|
|
trigger.Type = "github"
|
|
trigger.Code = authInfo.Code
|
|
trigger.OauthToken = OauthToken{
|
|
AccessToken: accessToken.AccessToken,
|
|
TokenType: accessToken.TokenType,
|
|
RefreshToken: accessToken.RefreshToken,
|
|
Expiry: accessToken.Expiry,
|
|
}
|
|
|
|
err = SetTriggerAuth(ctx, trigger)
|
|
if err != nil {
|
|
log.Printf("[WARNING] Failed to set trigger auth for %s - %s (github)", trigger.Username, err)
|
|
resp.WriteHeader(401)
|
|
return
|
|
}
|
|
|
|
_, err = HandleAlgoliaCreatorUpload(ctx, user, false, false)
|
|
if err != nil {
|
|
log.Printf("[ERROR] Failed making user %s' information public", user.Username)
|
|
}
|
|
|
|
log.Printf("Successful client setup for github?")
|
|
|
|
//if project.Environment == "cloud" && os.Getenv("CLOUD_ENVIRONMENT") != "local" {
|
|
resp.WriteHeader(200)
|
|
resp.Write([]byte(`{"success": true}`))
|
|
}
|
|
|
|
func HandleGetSpecificTrigger(resp http.ResponseWriter, request *http.Request) {
|
|
cors := HandleCors(resp, request)
|
|
if cors {
|
|
return
|
|
}
|
|
|
|
user, err := HandleApiAuthentication(resp, request)
|
|
if err != nil {
|
|
log.Printf("Api authentication failed in getting specific workflow: %s", err)
|
|
resp.WriteHeader(401)
|
|
resp.Write([]byte(`{"success": false}`))
|
|
return
|
|
}
|
|
|
|
location := strings.Split(request.URL.String(), "/")
|
|
|
|
var workflowId string
|
|
if location[1] == "api" {
|
|
if len(location) <= 4 {
|
|
resp.WriteHeader(401)
|
|
resp.Write([]byte(`{"success": false}`))
|
|
return
|
|
}
|
|
|
|
workflowId = location[5]
|
|
}
|
|
|
|
if strings.Contains(workflowId, "?") {
|
|
workflowId = strings.Split(workflowId, "?")[0]
|
|
}
|
|
|
|
ctx := GetContext(request)
|
|
trigger, err := GetTriggerAuth(ctx, workflowId)
|
|
if err != nil {
|
|
log.Printf("[INFO] Trigger %s doesn't exist - specific trigger.", workflowId)
|
|
resp.WriteHeader(403)
|
|
resp.Write([]byte(`{"success": false, "reason": ""}`))
|
|
return
|
|
}
|
|
|
|
if user.Username != trigger.Owner && user.Role != "admin" {
|
|
log.Printf("[AUDIT] Wrong user (%s) for trigger %s", user.Username, trigger.Id)
|
|
resp.WriteHeader(403)
|
|
return
|
|
}
|
|
|
|
trigger.OauthToken = OauthToken{}
|
|
trigger.Code = ""
|
|
|
|
b, err := json.Marshal(trigger)
|
|
if err != nil {
|
|
log.Println("Failed to marshal data")
|
|
resp.WriteHeader(401)
|
|
return
|
|
}
|
|
|
|
resp.WriteHeader(200)
|
|
resp.Write(b)
|
|
}
|
|
|
|
// Lists the users current subscriptions
|
|
func getOutlookSubscriptions(outlookClient *http.Client) (SubscriptionsWrapper, error) {
|
|
fullUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/subscriptions")
|
|
req, err := http.NewRequest(
|
|
"GET",
|
|
fullUrl,
|
|
nil,
|
|
)
|
|
req.Header.Add("Content-Type", "application/json")
|
|
res, err := outlookClient.Do(req)
|
|
if err != nil {
|
|
log.Printf("suberror Client: %s", err)
|
|
return SubscriptionsWrapper{}, err
|
|
}
|
|
|
|
defer res.Body.Close()
|
|
body, err := ioutil.ReadAll(res.Body)
|
|
if err != nil {
|
|
log.Printf("Suberror Body: %s", err)
|
|
return SubscriptionsWrapper{}, err
|
|
}
|
|
|
|
newSubs := SubscriptionsWrapper{}
|
|
err = json.Unmarshal(body, &newSubs)
|
|
if err != nil {
|
|
return SubscriptionsWrapper{}, err
|
|
}
|
|
|
|
return newSubs, nil
|
|
}
|
|
|
|
type SubscriptionsWrapper struct {
|
|
OdataContext string `json:"@odata.context"`
|
|
Value []OutlookSubscription `json:"value"`
|
|
}
|
|
|
|
type OutlookSubscription struct {
|
|
ChangeType string `json:"changeType"`
|
|
NotificationURL string `json:"notificationUrl"`
|
|
Resource string `json:"resource"`
|
|
ExpirationDateTime string `json:"expirationDateTime"`
|
|
ClientState string `json:"clientState"`
|
|
Id string `json:"id"`
|
|
}
|
|
|
|
type GmailSubscription struct {
|
|
TopicName string `json:"topicName"`
|
|
LabelIds []string `json:"labelIds"`
|
|
LabelFilterAction []string `json:"labelFilterAction"`
|
|
}
|
|
|
|
func GetGmailMessageAttachment(ctx context.Context, gmailClient *http.Client, userId, messageId, attachmentId string) (GmailAttachment, error) {
|
|
//fullUrl := fmt.Sprintf("https://gmail.googleapis.com/gmail/v1/users/%s/messages/%s?format=full", userId, messageId)
|
|
fullUrl := fmt.Sprintf("https://gmail.googleapis.com/gmail/v1/users/%s/messages/%s/attachments/%s", userId, messageId, attachmentId)
|
|
|
|
//fullUrl := fmt.Sprintf("https://gmail.googleapis.com/gmail/v1/users/me/messages/%s?format=full", messageId)
|
|
req, err := http.NewRequest(
|
|
"GET",
|
|
fullUrl,
|
|
nil,
|
|
)
|
|
req.Header.Add("Content-Type", "application/json")
|
|
res, err := gmailClient.Do(req)
|
|
if err != nil {
|
|
log.Printf("[WARNING] GMAIL get msg (4): %s", err)
|
|
return GmailAttachment{}, err
|
|
}
|
|
|
|
defer res.Body.Close()
|
|
log.Printf("[INFO] Get GMAIL attachment %#v Status: %d", messageId, res.StatusCode)
|
|
if res.StatusCode == 404 {
|
|
return GmailAttachment{}, errors.New(fmt.Sprintf("Failed to find mail for %s: %d", messageId, res.StatusCode))
|
|
}
|
|
|
|
body, err := ioutil.ReadAll(res.Body)
|
|
if err != nil {
|
|
log.Printf("[WARNING] Gmail get msg (5): %s", err)
|
|
return GmailAttachment{}, err
|
|
}
|
|
|
|
var message GmailAttachment
|
|
err = json.Unmarshal(body, &message)
|
|
if err != nil {
|
|
log.Printf("[WARNING] Failed body read unmarshal for gmail msg: %s", err)
|
|
return GmailAttachment{}, err
|
|
}
|
|
|
|
//log.Printf("ATTACHMENT MAIL WITH SIZE %d", message.Size)
|
|
|
|
//if len(profile.EmailAddress) == 0 {
|
|
// return GmailMessageStruct{}, errors.New("Couldn't find your email profile")
|
|
//}
|
|
|
|
//log.Printf("\n\nUSER BODY: %s", string(body))
|
|
return message, nil
|
|
}
|
|
|
|
func GetGithubRepoContributors(ctx context.Context, githubClient *http.Client, repo string) ([]GithubProfile, error) {
|
|
fullUrl := fmt.Sprintf("https://api.github.com/repos/%s/contributors", repo)
|
|
req, err := http.NewRequest(
|
|
"GET",
|
|
fullUrl,
|
|
nil,
|
|
)
|
|
|
|
req.Header.Add("Content-Type", "application/json")
|
|
res, err := githubClient.Do(req)
|
|
if err != nil {
|
|
log.Printf("[WARNING] Github user get (4): %s", err)
|
|
return []GithubProfile{}, err
|
|
}
|
|
|
|
defer res.Body.Close()
|
|
if res.StatusCode == 404 {
|
|
return []GithubProfile{}, errors.New(fmt.Sprintf("No repo contributors to get"))
|
|
}
|
|
|
|
body, err := ioutil.ReadAll(res.Body)
|
|
if err != nil {
|
|
log.Printf("[WARNING] Gmail get msg (5): %s", err)
|
|
return []GithubProfile{}, err
|
|
}
|
|
|
|
//log.Printf("PROFILE: %s", string(body))
|
|
var message []GithubProfile
|
|
err = json.Unmarshal(body, &message)
|
|
if err != nil {
|
|
log.Printf("[WARNING] Failed body read unmarshal for gmail msg: %s", err)
|
|
return []GithubProfile{}, err
|
|
}
|
|
|
|
return message, nil
|
|
}
|
|
|
|
func GetGithubProfile(ctx context.Context, githubClient *http.Client) (GithubProfile, error) {
|
|
fullUrl := fmt.Sprintf("https://api.github.com/user")
|
|
req, err := http.NewRequest(
|
|
"GET",
|
|
fullUrl,
|
|
nil,
|
|
)
|
|
req.Header.Add("Content-Type", "application/json")
|
|
res, err := githubClient.Do(req)
|
|
if err != nil {
|
|
log.Printf("[WARNING] Github user get (4): %s", err)
|
|
return GithubProfile{}, err
|
|
}
|
|
|
|
defer res.Body.Close()
|
|
if res.StatusCode == 404 {
|
|
return GithubProfile{}, errors.New(fmt.Sprintf("No user to get"))
|
|
}
|
|
|
|
body, err := ioutil.ReadAll(res.Body)
|
|
if err != nil {
|
|
log.Printf("[WARNING] Gmail get msg (5): %s", err)
|
|
return GithubProfile{}, err
|
|
}
|
|
|
|
//log.Printf("PROFILE: %s", string(body))
|
|
var message GithubProfile
|
|
err = json.Unmarshal(body, &message)
|
|
if err != nil {
|
|
log.Printf("[WARNING] Failed body read unmarshal for gmail msg: %s", err)
|
|
return GithubProfile{}, err
|
|
}
|
|
|
|
return message, nil
|
|
}
|
|
|
|
func GetGmailThread(ctx context.Context, gmailClient *http.Client, userId, messageId string) (GmailThreadStruct, error) {
|
|
fullUrl := fmt.Sprintf("https://gmail.googleapis.com/gmail/v1/users/%s/threads/%s?format=full", userId, messageId)
|
|
//fullUrl := fmt.Sprintf("https://gmail.googleapis.com/gmail/v1/users/me/messages/%s?format=full", messageId)
|
|
req, err := http.NewRequest(
|
|
"GET",
|
|
fullUrl,
|
|
nil,
|
|
)
|
|
req.Header.Add("Content-Type", "application/json")
|
|
res, err := gmailClient.Do(req)
|
|
if err != nil {
|
|
log.Printf("[WARNING] GMAIL get msg (4): %s", err)
|
|
return GmailThreadStruct{}, err
|
|
}
|
|
|
|
defer res.Body.Close()
|
|
log.Printf("[INFO] Get GMAIL thread %#v Status: %d", messageId, res.StatusCode)
|
|
if res.StatusCode == 404 {
|
|
return GmailThreadStruct{}, errors.New(fmt.Sprintf("Failed to find gmail thread for %s: %d", messageId, res.StatusCode))
|
|
}
|
|
|
|
body, err := ioutil.ReadAll(res.Body)
|
|
if err != nil {
|
|
log.Printf("[WARNING] Gmail get msg (5): %s", err)
|
|
return GmailThreadStruct{}, err
|
|
}
|
|
|
|
//log.Printf("THREAD: %s", string(body))
|
|
var message GmailThreadStruct
|
|
err = json.Unmarshal(body, &message)
|
|
if err != nil {
|
|
log.Printf("[WARNING] Failed body read unmarshal for gmail msg: %s", err)
|
|
return GmailThreadStruct{}, err
|
|
}
|
|
|
|
//if len(profile.EmailAddress) == 0 {
|
|
// return GmailMessageStruct{}, errors.New("Couldn't find your email profile")
|
|
//}
|
|
|
|
//log.Printf("\n\nUSER BODY: %s", string(body))
|
|
return message, nil
|
|
}
|
|
|
|
func GetGmailMessage(ctx context.Context, gmailClient *http.Client, userId, messageId string) (GmailMessageStruct, error) {
|
|
fullUrl := fmt.Sprintf("https://gmail.googleapis.com/gmail/v1/users/%s/messages/%s?format=full", userId, messageId)
|
|
//fullUrl := fmt.Sprintf("https://gmail.googleapis.com/gmail/v1/users/me/messages/%s?format=full", messageId)
|
|
req, err := http.NewRequest(
|
|
"GET",
|
|
fullUrl,
|
|
nil,
|
|
)
|
|
req.Header.Add("Content-Type", "application/json")
|
|
res, err := gmailClient.Do(req)
|
|
if err != nil {
|
|
log.Printf("[WARNING] GMAIL get msg (4): %s", err)
|
|
return GmailMessageStruct{}, err
|
|
}
|
|
|
|
defer res.Body.Close()
|
|
log.Printf("[INFO] Get GMAIL msg %#v Status: %d. User: %s", messageId, res.StatusCode, userId)
|
|
if res.StatusCode == 404 {
|
|
return GmailMessageStruct{}, errors.New(fmt.Sprintf("Failed to find mail for %s: %d", messageId, res.StatusCode))
|
|
}
|
|
|
|
body, err := ioutil.ReadAll(res.Body)
|
|
if err != nil {
|
|
log.Printf("[WARNING] Gmail get msg (5): %s", err)
|
|
return GmailMessageStruct{}, err
|
|
}
|
|
|
|
//log.Printf("MAIL: %s", string(body))
|
|
|
|
var message GmailMessageStruct
|
|
err = json.Unmarshal(body, &message)
|
|
if err != nil {
|
|
log.Printf("[WARNING] Failed body read unmarshal for gmail msg: %s", err)
|
|
return GmailMessageStruct{}, err
|
|
}
|
|
|
|
for _, header := range message.Payload.Headers {
|
|
if header.Name == "Subject" {
|
|
message.Payload.Subject = header.Value
|
|
}
|
|
if header.Name == "To" {
|
|
message.Payload.Recipient = header.Value
|
|
}
|
|
if header.Name == "From" {
|
|
message.Payload.Sender = header.Value
|
|
}
|
|
if header.Name == "Message-ID" {
|
|
message.Payload.MessageID = header.Value
|
|
|
|
if len(message.Payload.PartID) == 0 {
|
|
message.Payload.PartID = header.Value
|
|
}
|
|
}
|
|
}
|
|
|
|
message.Payload.Sender = strings.Replace(message.Payload.Sender, "\"", `'`, -1)
|
|
message.Payload.Subject = strings.Replace(message.Payload.Subject, "'", `'`, -1)
|
|
|
|
// Finding a parsed payload
|
|
for _, payload := range message.Payload.Parts {
|
|
//parsedBody = mess
|
|
//log.Printf("[DEBUG] Data to be decoded (%s): %d", payload.MimeType, len(payload.Body.Data))
|
|
if payload.MimeType == "text/plain" && payload.Filename == "" {
|
|
payload.Body.Data = strings.Replace(payload.Body.Data, "-", "+", -1)
|
|
payload.Body.Data = strings.Replace(payload.Body.Data, "_", "/", -1)
|
|
|
|
parsedData, err := base64.StdEncoding.DecodeString(payload.Body.Data)
|
|
if err != nil {
|
|
log.Printf("[WARNING] Failed base64 decode of parsedbody (text/plain): %s. New data length: %d. Using it anyway.", err, len(parsedData))
|
|
if len(parsedData) > 0 {
|
|
message.Payload.ParsedBody = string(parsedData)
|
|
continue
|
|
//break
|
|
}
|
|
|
|
if len(message.Payload.ParsedBody) == 0 {
|
|
message.Payload.ParsedBody = string(parsedData)
|
|
}
|
|
|
|
continue
|
|
}
|
|
|
|
message.Payload.ParsedBody = string(parsedData)
|
|
} else {
|
|
if len(payload.Filename) > 0 {
|
|
message.Payload.Filename = payload.Filename
|
|
message.Payload.FileMimeType = payload.MimeType
|
|
} else if len(message.Payload.ParsedBody) == 0 {
|
|
message.Payload.ParsedBody = string(payload.Body.Data)
|
|
}
|
|
}
|
|
|
|
if len(message.Payload.ParsedBody) > 0 && message.Payload.FileMimeType == "" {
|
|
message.Payload.FileMimeType = payload.MimeType
|
|
}
|
|
}
|
|
|
|
//log.Printf("\n\nUSER BODY: %s", string(body))
|
|
return message, nil
|
|
}
|
|
|
|
type CodeVerifier struct {
|
|
Value string
|
|
}
|
|
|
|
const (
|
|
length = 32
|
|
)
|
|
|
|
func CreateCodeVerifierFromBytes(b []byte) (*CodeVerifier, error) {
|
|
return &CodeVerifier{
|
|
Value: base64URLEncode(b),
|
|
}, nil
|
|
}
|
|
|
|
func base64URLEncode(str []byte) string {
|
|
encoded := base64.StdEncoding.EncodeToString(str)
|
|
encoded = strings.Replace(encoded, "+", "-", -1)
|
|
encoded = strings.Replace(encoded, "/", "_", -1)
|
|
encoded = strings.Replace(encoded, "=", "", -1)
|
|
return encoded
|
|
}
|
|
|
|
func (v *CodeVerifier) CodeChallengeS256() string {
|
|
h := sha256.New()
|
|
h.Write([]byte(v.Value))
|
|
return base64URLEncode(h.Sum(nil))
|
|
}
|
|
|
|
// https://dev-18062.okta.com/oauth2/default/v1/authorize?client_id=0oa3&response_type=code&scope=openid&redirect_uri=http%3A%2F%2Flocalhost%3A5002%2Fapi%2Fv1%2Flogin_openid&state=state-296bc9a0-a2a2-4a57-be1a-d0e2fd9bb601&code_challenge_method=S256&code_challenge=codechallenge
|
|
func RunOpenidLogin(ctx context.Context, clientId, baseUrl, redirectUri, code, codeChallenge, clientSecret string) ([]byte, error) {
|
|
if len(codeChallenge) == 0 {
|
|
return []byte{}, errors.New("code challenge is required")
|
|
}
|
|
|
|
client := &http.Client{}
|
|
data := fmt.Sprintf("client_id=%s&grant_type=authorization_code&redirect_uri=%s&code=%s&code_verifier=%s", clientId, redirectUri, code, codeChallenge)
|
|
if len(clientSecret) > 0 {
|
|
data += fmt.Sprintf("&client_secret=%s", clientSecret)
|
|
}
|
|
|
|
req, err := http.NewRequest(
|
|
"POST",
|
|
baseUrl,
|
|
bytes.NewBuffer([]byte(data)),
|
|
)
|
|
|
|
req.Header.Add("content-type", "application/x-www-form-urlencoded")
|
|
req.Header.Add("accept", "application/json")
|
|
req.Header.Add("cache-control", "no-cache")
|
|
res, err := client.Do(req)
|
|
if err != nil {
|
|
log.Printf("[WARNING] OpenID Client: %s", err)
|
|
return []byte{}, err
|
|
}
|
|
|
|
defer res.Body.Close()
|
|
body, err := ioutil.ReadAll(res.Body)
|
|
if err != nil {
|
|
log.Printf("[WARNING] OpenID client Body: %s", err)
|
|
return []byte{}, err
|
|
}
|
|
|
|
log.Printf("OpenID return BODY: %s", body)
|
|
|
|
return body, nil
|
|
}
|
|
|
|
func GetGithubClient(ctx context.Context, code string, accessToken OauthToken, redirectUri string) (*http.Client, *oauth2.Token, error) {
|
|
//fullUrl := fmt.Sprintf("https://github.com/login/oauth/access_token?client_id=%s&client_secret=%s&code=%s&token_type=bearer", os.Getenv("GITHUB_CLIENT"), os.Getenv("GITHUB_SECRET"), code)
|
|
//log.Printf("Posting to URL %s for github", fullUrl)
|
|
//client := &http.Client{
|
|
// Timeout: 1 * time.Second,
|
|
//}
|
|
//req, err := http.NewRequest(
|
|
// "POST",
|
|
// fullUrl,
|
|
// nil,
|
|
//)
|
|
|
|
//req.Header.Add("Content-Type", "application/json")
|
|
//res, err := client.Do(req)
|
|
//if err != nil {
|
|
// log.Printf("[WARNING] GMAIL Client: %s", err)
|
|
// return &http.Client{}, &oauth2.Token{}, err
|
|
//}
|
|
|
|
//body, err := ioutil.ReadAll(res.Body)
|
|
//if err != nil {
|
|
// log.Printf("[WARNING] Gmail subscription Body: %s", err)
|
|
// return &http.Client{}, &oauth2.Token{}, err
|
|
//}
|
|
|
|
//log.Printf("BODY: %s", body)
|
|
|
|
//return &http.Client{}, &oauth2.Token{}, err
|
|
|
|
//RedirectURL: "http://localhost:3002/set_authentication",
|
|
conf := &oauth2.Config{
|
|
ClientID: os.Getenv("GITHUB_CLIENT"),
|
|
ClientSecret: os.Getenv("GITHUB_SECRET"),
|
|
Scopes: []string{
|
|
"read:user",
|
|
//"repo",
|
|
},
|
|
Endpoint: oauth2.Endpoint{
|
|
AuthURL: "https://github.com/login/oauth/authorize",
|
|
TokenURL: "https://github.com/login/oauth/access_token",
|
|
},
|
|
}
|
|
|
|
log.Printf("CONF: %#v", conf)
|
|
|
|
if len(code) > 0 {
|
|
access_token, err := conf.Exchange(ctx, code)
|
|
if err != nil {
|
|
log.Printf("[WARNING] Access_token issue for Github: %s", err)
|
|
return &http.Client{}, access_token, err
|
|
}
|
|
|
|
client := conf.Client(ctx, access_token)
|
|
return client, access_token, nil
|
|
}
|
|
|
|
// Manually recreate the oauthtoken
|
|
access_token := &oauth2.Token{
|
|
AccessToken: accessToken.AccessToken,
|
|
TokenType: accessToken.TokenType,
|
|
RefreshToken: accessToken.RefreshToken,
|
|
Expiry: accessToken.Expiry,
|
|
}
|
|
|
|
client := conf.Client(ctx, access_token)
|
|
return client, access_token, nil
|
|
}
|
|
|
|
// THis all of a sudden became really horrible.. fml
|
|
func GetGmailClient(ctx context.Context, code string, accessToken OauthToken, redirectUri string) (*http.Client, *oauth2.Token, error) {
|
|
clientId := os.Getenv("GMAIL_CLIENT_ID")
|
|
clientSecret := os.Getenv("GMAIL_CLIENT_SECRET")
|
|
|
|
conf := &oauth2.Config{
|
|
ClientID: clientId,
|
|
ClientSecret: clientSecret,
|
|
Scopes: []string{
|
|
"https://www.googleapis.com/auth/gmail.readonly",
|
|
},
|
|
RedirectURL: redirectUri,
|
|
Endpoint: oauth2.Endpoint{
|
|
AuthURL: "https://accounts.google.com/o/oauth2/auth",
|
|
TokenURL: "https://accounts.google.com/o/oauth2/token",
|
|
},
|
|
}
|
|
|
|
if len(code) > 0 {
|
|
access_token, err := conf.Exchange(ctx, code)
|
|
if err != nil {
|
|
log.Printf("[WARNING] Access_token issue for Gmail: %s", err)
|
|
return &http.Client{}, access_token, err
|
|
}
|
|
|
|
client := conf.Client(ctx, access_token)
|
|
return client, access_token, nil
|
|
}
|
|
|
|
// Manually recreate the oauthtoken
|
|
access_token := &oauth2.Token{
|
|
AccessToken: accessToken.AccessToken,
|
|
TokenType: accessToken.TokenType,
|
|
RefreshToken: accessToken.RefreshToken,
|
|
Expiry: accessToken.Expiry,
|
|
}
|
|
|
|
client := conf.Client(ctx, access_token)
|
|
return client, access_token, nil
|
|
}
|
|
|
|
// THis all of a sudden became really horrible.. fml
|
|
func GetOutlookClient(ctx context.Context, code string, accessToken OauthToken, redirectUri string) (*http.Client, *oauth2.Token, error) {
|
|
conf := &oauth2.Config{
|
|
ClientID: os.Getenv("OFFICE365_CLIENT_ID"),
|
|
ClientSecret: os.Getenv("OFFICE365_CLIENT_SECRET"),
|
|
Scopes: []string{
|
|
"Mail.Read",
|
|
},
|
|
RedirectURL: redirectUri,
|
|
Endpoint: oauth2.Endpoint{
|
|
AuthURL: "https://login.microsoftonline.com/common/oauth2/authorize",
|
|
TokenURL: "https://login.microsoftonline.com/common/oauth2/token",
|
|
},
|
|
}
|
|
|
|
if len(code) > 0 {
|
|
access_token, err := conf.Exchange(ctx, code)
|
|
if err != nil {
|
|
log.Printf("[ERROR] Access_token issue for Outlook: %s", err)
|
|
return &http.Client{}, access_token, err
|
|
}
|
|
|
|
client := conf.Client(ctx, access_token)
|
|
return client, access_token, nil
|
|
}
|
|
|
|
// Manually recreate the oauthtoken
|
|
access_token := &oauth2.Token{
|
|
AccessToken: accessToken.AccessToken,
|
|
TokenType: accessToken.TokenType,
|
|
RefreshToken: accessToken.RefreshToken,
|
|
Expiry: accessToken.Expiry,
|
|
}
|
|
|
|
client := conf.Client(ctx, access_token)
|
|
return client, access_token, nil
|
|
}
|
|
|
|
func GetGmailFolders(client *http.Client) (OutlookFolders, error) {
|
|
//requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/users/ec03b4f2-fccf-4c35-b0eb-be85a0f5dd43/mailFolders")
|
|
requestUrl := fmt.Sprintf("https://gmail.googleapis.com/gmail/v1/users/me/labels")
|
|
|
|
ret, err := client.Get(requestUrl)
|
|
if err != nil {
|
|
log.Printf("[INFO] FolderErr gmail: %s", err)
|
|
return OutlookFolders{}, err
|
|
}
|
|
|
|
body, err := ioutil.ReadAll(ret.Body)
|
|
if err != nil {
|
|
log.Printf("[WARNING] Failed body decoding from mailfolders")
|
|
return OutlookFolders{}, err
|
|
}
|
|
|
|
//log.Printf("Folders: %s", string(body))
|
|
//log.Printf("[INFO] Folder Body: %s", string(body))
|
|
if ret.StatusCode != 200 {
|
|
log.Printf("[INFO] Bad Status for GMAIL folders (Labels): %d. Body: %s", ret.StatusCode, string(body))
|
|
return OutlookFolders{}, err
|
|
}
|
|
|
|
labels := GmailLabels{}
|
|
err = json.Unmarshal(body, &labels)
|
|
if err != nil {
|
|
log.Printf("[WARNING] GMAIL folder Unmarshal: %s", err)
|
|
return OutlookFolders{}, err
|
|
}
|
|
|
|
// Casting to Outlook for frontend usability reasons
|
|
log.Printf("[DEBUG] Found %d labels", len(labels.Labels))
|
|
mailfolders := OutlookFolders{}
|
|
for _, label := range labels.Labels {
|
|
if label.MessageListVisibility == "hide" {
|
|
continue
|
|
}
|
|
|
|
mailfolders.Value = append(mailfolders.Value, OutlookFolder{
|
|
ID: label.ID,
|
|
DisplayName: label.Name,
|
|
})
|
|
}
|
|
|
|
//fmt.Printf("%#v", mailfolders)
|
|
// FIXME - recursion for subfolders
|
|
// Recursive struct
|
|
// folderEndpoint := fmt.Sprintf("%s/%s/childfolders?$top=40", requestUrl, parentId)
|
|
//for _, folder := range mailfolders.Value {
|
|
// log.Println(folder.DisplayName)
|
|
//}
|
|
|
|
return mailfolders, nil
|
|
}
|
|
|
|
func getOutlookFolders(client *http.Client) (OutlookFolders, error) {
|
|
//requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/users/ec03b4f2-fccf-4c35-b0eb-be85a0f5dd43/mailFolders")
|
|
//requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/me/mailFolders")
|
|
|
|
// Include hidden folders
|
|
requestUrl := fmt.Sprintf("https://graph.microsoft.com/beta/me/mailFolders?$top=100&$expand=childFolders")
|
|
|
|
ret, err := client.Get(requestUrl)
|
|
if err != nil {
|
|
log.Printf("[INFO] FolderErr: %s", err)
|
|
return OutlookFolders{}, err
|
|
}
|
|
|
|
body, err := ioutil.ReadAll(ret.Body)
|
|
if err != nil {
|
|
log.Printf("[WARNING] Failed body decoding from mailfolders")
|
|
return OutlookFolders{}, err
|
|
}
|
|
|
|
//log.Printf("[INFO] Folder Body: %s", string(body))
|
|
log.Printf("[INFO] Status Outlook folders: %d. Reason: %s", ret.StatusCode, string(body))
|
|
if ret.StatusCode != 200 {
|
|
return OutlookFolders{}, err
|
|
}
|
|
|
|
//log.Printf("Body: %s", string(body))
|
|
|
|
mailfolders := OutlookFolders{}
|
|
err = json.Unmarshal(body, &mailfolders)
|
|
if err != nil {
|
|
log.Printf("Unmarshal: %s", err)
|
|
return OutlookFolders{}, err
|
|
}
|
|
|
|
//fmt.Printf("%#v", mailfolders)
|
|
// FIXME - recursion for subfolders
|
|
// Recursive struct
|
|
// folderEndpoint := fmt.Sprintf("%s/%s/childfolders?$top=40", requestUrl, parentId)
|
|
//for _, folder := range mailfolders.Value {
|
|
// log.Println(folder.DisplayName)
|
|
//}
|
|
|
|
return mailfolders, nil
|
|
}
|
|
|
|
func GetOauth2ApplicationPermissionToken(ctx context.Context, user User, appAuth AppAuthenticationStorage) (AppAuthenticationStorage, error) {
|
|
// transport := http.DefaultTransport.(*http.Transport)
|
|
transport := http.DefaultTransport.(*http.Transport).Clone()
|
|
transport.MaxIdleConnsPerHost = 100
|
|
transport.ResponseHeaderTimeout = time.Second * 10
|
|
transport.Proxy = nil
|
|
|
|
clientId := ""
|
|
clientSecret := ""
|
|
tokenUrl := ""
|
|
scope := ""
|
|
|
|
grantType := "client_credentials"
|
|
username := ""
|
|
password := ""
|
|
|
|
//log.Printf("[DEBUG] Got %d auth fields (%s)", len(appAuth.Fields), appAuth.Id)
|
|
for _, field := range appAuth.Fields {
|
|
if field.Key == "client_secret" {
|
|
clientSecret = field.Value
|
|
} else if field.Key == "client_id" {
|
|
clientId = field.Value
|
|
} else if field.Key == "scope" {
|
|
scope = field.Value
|
|
} else if field.Key == "token_uri" {
|
|
tokenUrl = field.Value
|
|
} else if field.Key == "grant_type" {
|
|
grantType = field.Value
|
|
} else if field.Key == "username" {
|
|
username = field.Value
|
|
} else if field.Key == "password" {
|
|
password = field.Value
|
|
} else {
|
|
}
|
|
}
|
|
|
|
if len(tokenUrl) == 0 || len(clientId) == 0 || len(clientSecret) == 0 {
|
|
return appAuth, fmt.Errorf("Missing oauth2 fields. Required: token_uri, client_id, client_secret, scopes")
|
|
}
|
|
|
|
zscalerAuth := strings.Contains(tokenUrl, ".zslogin.net")
|
|
if zscalerAuth && len(scope) == 0 {
|
|
scope = "https://api.zscaler.com"
|
|
}
|
|
|
|
refreshData := fmt.Sprintf("grant_type=client_credentials")
|
|
if len(grantType) > 0 {
|
|
refreshData = fmt.Sprintf("grant_type=%s", grantType)
|
|
}
|
|
|
|
if grantType == "password" {
|
|
if len(username) > 0 {
|
|
refreshData += fmt.Sprintf("&username=%s", username)
|
|
}
|
|
|
|
if len(password) > 0 {
|
|
refreshData += fmt.Sprintf("&password=%s", password)
|
|
}
|
|
|
|
refreshData += fmt.Sprintf("&client_id=%s", clientId)
|
|
refreshData += fmt.Sprintf("&client_secret=%s", clientSecret)
|
|
}
|
|
|
|
if grantType == "client_credentials" && zscalerAuth {
|
|
refreshData += fmt.Sprintf("&client_id=%s", clientId)
|
|
refreshData += fmt.Sprintf("&client_secret=%s", clientSecret)
|
|
}
|
|
|
|
if len(scope) > 0 {
|
|
if zscalerAuth {
|
|
refreshData += fmt.Sprintf("&audience=%s", strings.Replace(scope, ",", " ", -1))
|
|
} else {
|
|
refreshData += fmt.Sprintf("&scope=%s", strings.Replace(scope, ",", " ", -1))
|
|
}
|
|
}
|
|
|
|
if strings.Contains(refreshData, "user_impersonation") && strings.Contains(refreshData, "azure") && !strings.Contains(refreshData, "resource=") {
|
|
// Add "resource" for microsoft hings
|
|
refreshData += "&resource=https://management.azure.com"
|
|
}
|
|
|
|
// Not necessary for refresh
|
|
log.Printf("[DEBUG] Oauth2 REFRESH DATA: %#v. URL: %#v", refreshData, tokenUrl)
|
|
|
|
client := GetExternalClient(tokenUrl)
|
|
req, err := http.NewRequest(
|
|
"POST",
|
|
tokenUrl,
|
|
bytes.NewBuffer([]byte(refreshData)),
|
|
)
|
|
|
|
if err != nil {
|
|
return appAuth, err
|
|
}
|
|
|
|
// Basic auth handler for client_credentials. May not always be the case, it's currently used by default
|
|
if grantType == "client_credentials" && !zscalerAuth {
|
|
authHeader := fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", clientId, clientSecret))))
|
|
req.Header.Set("Authorization", authHeader)
|
|
}
|
|
|
|
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
|
req.Header.Add("Accept", "application/json")
|
|
newresp, err := client.Do(req)
|
|
if err != nil {
|
|
return appAuth, err
|
|
}
|
|
|
|
defer newresp.Body.Close()
|
|
body, err := ioutil.ReadAll(newresp.Body)
|
|
if err != nil {
|
|
log.Printf("[ERROR] Oauth2 application auth: Failed to read response body: %s", err)
|
|
return appAuth, err
|
|
}
|
|
|
|
log.Printf("[DEBUG] Oauth2 application auth Response for %s: %d", tokenUrl, newresp.StatusCode)
|
|
|
|
if newresp.StatusCode >= 300 {
|
|
// Printing on error to handle in future instances
|
|
log.Printf("[ERROR] Oauth2 application data for %s: %#v", tokenUrl, string(body))
|
|
|
|
// Autocorrecting scopes -> audience
|
|
if strings.Contains(string(body), "error") && strings.Contains(string(body), "audience") && len(scope) > 0 {
|
|
log.Printf("[INFO] Oauth2 application auth: Autocorrecting scopes -> audience")
|
|
|
|
refreshData = fmt.Sprintf("grant_type=client_credentials")
|
|
if len(grantType) > 0 {
|
|
refreshData = fmt.Sprintf("grant_type=%s", grantType)
|
|
}
|
|
|
|
refreshData += fmt.Sprintf("&audience=%s", strings.Replace(scope, ",", " ", -1))
|
|
req.Body = ioutil.NopCloser(bytes.NewBuffer([]byte(refreshData)))
|
|
req.ContentLength = int64(len(refreshData))
|
|
|
|
if !zscalerAuth {
|
|
authHeader := fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", clientId, clientSecret))))
|
|
req.Header.Set("Authorization", authHeader)
|
|
}
|
|
|
|
newresp, err = client.Do(req)
|
|
if err != nil {
|
|
log.Printf("[ERROR] Oauth2 application auth (2): Failed to autocorrect scopes -> audience: %s", err)
|
|
return appAuth, err
|
|
}
|
|
|
|
defer newresp.Body.Close()
|
|
body, err = ioutil.ReadAll(newresp.Body)
|
|
if err != nil {
|
|
log.Printf("[ERROR] Oauth2 application auth (3): Failed to read response body: %s", err)
|
|
return appAuth, err
|
|
}
|
|
}
|
|
|
|
// Takes care of both old and new request
|
|
if newresp.StatusCode >= 300 {
|
|
return appAuth, errors.New(fmt.Sprintf("Bad status code when getting access token for token URL %s: %d. Message: %s", tokenUrl, newresp.StatusCode, body))
|
|
}
|
|
}
|
|
|
|
if strings.Contains(string(body), "error") {
|
|
log.Printf("\n\n[ERROR] Oauth2 app RESPONSE: %s\n\n", string(body))
|
|
}
|
|
|
|
// Parse out data like {"access_token":"ddpGSlBV4GhNhToPTLjHZSwbqRH6JUIv0QYPo6CW62NfAr","token_type":"Bearer","expires_in":1870}
|
|
var data map[string]interface{}
|
|
err = json.Unmarshal(body, &data)
|
|
if err != nil {
|
|
return appAuth, err
|
|
}
|
|
|
|
//log.Printf("[DEBUG] Oauth2 data for %s: %d", tokenUrl, newresp.StatusCode)
|
|
// Check if access_token is in data
|
|
foundToken := ""
|
|
if _, ok := data["access_token"]; !ok {
|
|
return appAuth, errors.New(fmt.Sprintf("Missing access_token in response from %s", tokenUrl))
|
|
} else {
|
|
foundToken = data["access_token"].(string)
|
|
}
|
|
|
|
if len(foundToken) == 0 {
|
|
return appAuth, errors.New(fmt.Sprintf("Empty access_token in response from %s", tokenUrl))
|
|
}
|
|
|
|
appAuth.Fields = append(appAuth.Fields, AuthenticationStore{
|
|
Key: "access_token",
|
|
Value: foundToken,
|
|
})
|
|
|
|
return appAuth, nil
|
|
}
|
|
|
|
func RunOauth2Request(ctx context.Context, user User, appAuth AppAuthenticationStorage, refresh bool) (AppAuthenticationStorage, error) {
|
|
|
|
transport := http.DefaultTransport.(*http.Transport).Clone()
|
|
// transport := http.DefaultTransport.(*http.Transport)
|
|
transport.MaxIdleConnsPerHost = 100
|
|
transport.ResponseHeaderTimeout = time.Second * 10
|
|
transport.Proxy = nil
|
|
|
|
requestData := DataToSend{
|
|
GrantType: "authorization_code",
|
|
}
|
|
|
|
url := ""
|
|
oauthUrl := ""
|
|
refreshUrl := ""
|
|
refreshToken := ""
|
|
|
|
for _, field := range appAuth.Fields {
|
|
// Try decryption here as well just in case
|
|
// In some cases, it's already decrypted at this point, but it doesn't matter much to re-do it in case, as this function is used multiple places
|
|
decryptionKey := fmt.Sprintf("%s_%d_%s_%s", appAuth.OrgId, appAuth.Created, appAuth.Label, field.Key)
|
|
newValue, err := HandleKeyDecryption([]byte(field.Value), decryptionKey)
|
|
if err == nil {
|
|
field.Value = string(newValue)
|
|
} else {
|
|
//log.Printf("[DEBUG] Failed decrypting field %s: %s", field.Key, err)
|
|
}
|
|
|
|
if field.Key == "authentication_url" {
|
|
url = field.Value
|
|
} else if field.Key == "code" {
|
|
requestData.Code = field.Value
|
|
} else if field.Key == "client_secret" {
|
|
requestData.ClientSecret = field.Value
|
|
} else if field.Key == "client_id" {
|
|
requestData.ClientId = field.Value
|
|
} else if field.Key == "scopes" {
|
|
requestData.Scope = field.Value
|
|
} else if field.Key == "scope" {
|
|
requestData.Scope = field.Value
|
|
} else if field.Key == "redirect_uri" {
|
|
|
|
requestData.RedirectUri = field.Value
|
|
} else if field.Key == "refresh_uri" || field.Key == "refresh_url" {
|
|
refreshUrl = field.Value
|
|
} else if field.Key == "refresh_token" {
|
|
//log.Printf("[DEBUG] Got refresh token %s", field.Value)
|
|
refreshToken = field.Value
|
|
} else if field.Key == "oauth_url" {
|
|
oauthUrl = field.Value
|
|
} else {
|
|
if field.Key == "url" {
|
|
} else {
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(requestData.ClientSecret) == 0 && len(requestData.ClientId) > 0 {
|
|
oauth2data, err := GetHostedOAuth(ctx, requestData.ClientId)
|
|
if err == nil && len(oauth2data.ClientSecret) > 0 {
|
|
requestData.ClientSecret = oauth2data.ClientSecret
|
|
}
|
|
}
|
|
|
|
//log.Printf("[DEBUG] Making request with auth %s to %s for Oauth2 token. User: '%s' ('%s')", appAuth.Id, url, user.Username, user.Id)
|
|
//log.Printf("[DEBUG] Verbose Requestdata: Sending request to %#v with requestdata %#v", url, requestData)
|
|
if len(url) == 0 {
|
|
return appAuth, errors.New("No authentication URL provided in Oauth2 request")
|
|
}
|
|
|
|
if len(requestData.Resource) == 0 {
|
|
if strings.Contains(url, "microsoft") {
|
|
//log.Printf("[DEBUG] Should look to add add resource to the query data for URL %s. Resource: %#v", url, requestData.Resource)
|
|
foundScope := ""
|
|
for _, scope := range strings.Split(requestData.Scope, " ") {
|
|
if strings.Contains(string(scope), "https://") {
|
|
foundScope = string(scope)
|
|
break
|
|
}
|
|
}
|
|
|
|
if len(foundScope) > 0 {
|
|
scopeSplit := strings.Split(foundScope, "/")
|
|
|
|
if len(scopeSplit) > 2 {
|
|
//requestData.Resource = "https://management.azure.com/"
|
|
requestData.Resource = strings.Join(scopeSplit[0:3], "/") + "/"
|
|
log.Printf("[DEBUG] Set resource to be %#v from SCOPES: %#v", requestData.Resource, requestData.Scope)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// To send: POST
|
|
// URL sample: https://login.microsoftonline.com/b6eb57ed-ecfc-4af2-b0ff-467a2e2c806f/oauth2/v2.0/token
|
|
// Data to be sent: requestData formatted?
|
|
v, err := query.Values(requestData)
|
|
if err != nil {
|
|
log.Printf("[ERROR] Failed parsing Oauth2 values: %s", err)
|
|
return appAuth, err
|
|
}
|
|
|
|
if len(refreshToken) == 0 && refresh {
|
|
refresh = false
|
|
}
|
|
|
|
// Look for {tenant in the URL. If it's found, find the next } after it, then replace it with 'common'
|
|
// This is to make sure to handle tenant things for microsoft
|
|
if strings.Contains(strings.ToLower(url), "{tenant") {
|
|
//log.Printf("[DEBUG] Found tenant in URL: %s", url)
|
|
tenantPos := strings.Index(strings.ToLower(url), "{tenant")
|
|
|
|
if tenantPos >= 0 {
|
|
tenantEnd := strings.Index(url[tenantPos:], "}")
|
|
if tenantEnd >= 0 {
|
|
url = url[:tenantPos] + "common" + url[tenantPos+tenantEnd+1:]
|
|
//log.Printf("[DEBUG] Replaced tenant in URL: %s", url)
|
|
}
|
|
}
|
|
}
|
|
|
|
client := GetExternalClient(url)
|
|
newresp := &http.Response{}
|
|
respBody := []byte{}
|
|
if !refresh {
|
|
req, err := http.NewRequest(
|
|
"POST",
|
|
url,
|
|
bytes.NewBuffer([]byte(v.Encode())),
|
|
)
|
|
|
|
if err != nil {
|
|
log.Printf("[ERROR] Failed setting up Oauth2 request for %s: %s", url, err)
|
|
return appAuth, err
|
|
}
|
|
|
|
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
|
req.Header.Add("Accept", "application/json")
|
|
newresp, err = client.Do(req)
|
|
if err != nil {
|
|
log.Printf("[ERROR] Failed running Oauth2 request for %s: %s", url, err)
|
|
return appAuth, err
|
|
}
|
|
|
|
//log.Printf("Data: %#v", newresp)
|
|
//log.Printf("Data: %d", newresp.StatusCode)
|
|
|
|
defer newresp.Body.Close()
|
|
body, err := ioutil.ReadAll(newresp.Body)
|
|
if err != nil {
|
|
log.Printf("[ERROR] Failed unmarshalling body from Oauth2 request for %s: %s", url, err)
|
|
return appAuth, err
|
|
}
|
|
|
|
respBody = body
|
|
if newresp.StatusCode >= 300 {
|
|
return appAuth, errors.New(fmt.Sprintf("Bad status code for URL (NOT refresh) %s: %d. Message: %s", url, newresp.StatusCode, respBody))
|
|
}
|
|
} else {
|
|
|
|
if len(refreshToken) == 0 {
|
|
log.Printf("[ERROR] No refresh token acquired for %s", refreshUrl)
|
|
return appAuth, errors.New("No refresh token specified during initial auth.")
|
|
}
|
|
|
|
requestRefreshUrl := fmt.Sprintf("%s", refreshUrl)
|
|
refreshData := fmt.Sprintf("grant_type=refresh_token&refresh_token=%s&scope=%s&client_id=%s&client_secret=%s", refreshToken, strings.Replace(requestData.Scope, " ", "%20", -1), requestData.ClientId, requestData.ClientSecret)
|
|
|
|
// This is to make sure to handle tenant things for microsoft
|
|
if strings.Contains(strings.ToLower(requestRefreshUrl), "{tenant") {
|
|
//log.Printf("[DEBUG] Found tenant in URL: %s", url)
|
|
tenantPos := strings.Index(strings.ToLower(requestRefreshUrl), "{tenant")
|
|
|
|
if tenantPos >= 0 {
|
|
tenantEnd := strings.Index(requestRefreshUrl[tenantPos:], "}")
|
|
if tenantEnd >= 0 {
|
|
requestRefreshUrl = requestRefreshUrl[:tenantPos] + "common" + requestRefreshUrl[tenantPos+tenantEnd+1:]
|
|
//log.Printf("[DEBUG] Replaced tenant in URL: %s", requestRefreshUrl)
|
|
}
|
|
}
|
|
}
|
|
|
|
//log.Printf("[DEBUG] Refresh URL: %s?%s", requestRefreshUrl, refreshData)
|
|
req, err := http.NewRequest(
|
|
"POST",
|
|
requestRefreshUrl,
|
|
bytes.NewBuffer([]byte(refreshData)),
|
|
)
|
|
|
|
if err != nil {
|
|
return appAuth, err
|
|
}
|
|
|
|
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
|
req.Header.Add("Accept", "application/json")
|
|
newresp, err = client.Do(req)
|
|
if err != nil {
|
|
return appAuth, err
|
|
}
|
|
|
|
defer newresp.Body.Close()
|
|
body, err := ioutil.ReadAll(newresp.Body)
|
|
if err != nil {
|
|
return appAuth, err
|
|
}
|
|
|
|
respBody = body
|
|
|
|
if newresp.StatusCode >= 300 {
|
|
// Printing on error to handle in future instances
|
|
//log.Printf("[ERROR] Oauth2 data for %s: %#v", requestRefreshUrl, newresp)
|
|
return appAuth, errors.New(fmt.Sprintf("Bad status code in refresh for URL (refresh) %s: %d. Message: %s", url, newresp.StatusCode, respBody))
|
|
}
|
|
|
|
// Overwriting auth
|
|
newAuth := []AuthenticationStore{}
|
|
for _, item := range appAuth.Fields {
|
|
if item.Key == "access_token" || item.Value == "expiration" || item.Value == "expires_in" {
|
|
continue
|
|
}
|
|
|
|
newAuth = append(newAuth, item)
|
|
}
|
|
|
|
appAuth.Fields = newAuth
|
|
}
|
|
|
|
if strings.Contains(string(respBody), "error") {
|
|
//log.Printf("\n\n[ERROR] Oauth2 RESPONSE: %s\n\nencoded: %#v\n", string(respBody), v.Encode())
|
|
log.Printf("[ERROR] Bad Oauth2 RESPONSE (%d) from %s: %s. Auth ID: %s", newresp.StatusCode, url, string(respBody), appAuth.Id)
|
|
|
|
go CreateOrgNotification(
|
|
context.Background(),
|
|
fmt.Sprintf("Oauth2 error during refresh of URL %s at the start of workflow", url),
|
|
fmt.Sprintf("Error during Oauth2 refresh (%d): %s", newresp.StatusCode, string(respBody)),
|
|
fmt.Sprintf("/admin?admin_tab=notifications"),
|
|
appAuth.OrgId,
|
|
true,
|
|
"HIGH",
|
|
"oauth",
|
|
)
|
|
|
|
if newresp.StatusCode >= 300 {
|
|
return appAuth, errors.New(fmt.Sprintf("Bad response from Oauth2 request for %s: %s", url, string(respBody)))
|
|
}
|
|
}
|
|
|
|
// Check if we have an authentication token and pre-set it
|
|
var oauthResp Oauth2Resp
|
|
for _, field := range appAuth.Fields {
|
|
if field.Key == "access_token" {
|
|
oauthResp.AccessToken = field.Value
|
|
break
|
|
}
|
|
}
|
|
|
|
err = json.Unmarshal(respBody, &oauthResp)
|
|
if err != nil {
|
|
if len(oauthResp.AccessToken) == 0 {
|
|
log.Printf("[ERROR] Failed unmarshaling (appauth oauth2 refresh). URL: %#v: %s. Data: %s. Trying to map to oauthResp anyway", url, respBody, err)
|
|
changed := false
|
|
if strings.Contains(string(respBody), "access_token") {
|
|
for _, item := range strings.Split(string(respBody), "&") {
|
|
if !strings.Contains(item, "=") {
|
|
continue
|
|
}
|
|
|
|
changed = true
|
|
if strings.Contains(item, "access_token") {
|
|
oauthResp.AccessToken = strings.Split(item, "=")[1]
|
|
}
|
|
|
|
if strings.Contains(item, "scope") {
|
|
oauthResp.Scope = strings.Split(item, "=")[1]
|
|
}
|
|
|
|
if strings.Contains(item, "token_type") {
|
|
oauthResp.TokenType = strings.Split(item, "=")[1]
|
|
}
|
|
|
|
if strings.Contains(item, "refresh_token") || strings.Contains(item, "refresh") {
|
|
oauthResp.RefreshToken = strings.Split(item, "=")[1]
|
|
}
|
|
}
|
|
}
|
|
|
|
if !changed {
|
|
return appAuth, err
|
|
}
|
|
} else {
|
|
log.Printf("[ERROR] Failed unmarshaling (appauth oauth2) (2): %s. Continuing anyway as we have an access token", err)
|
|
}
|
|
}
|
|
|
|
// Need to refresh the "code"? Is that a thing?
|
|
//log.Printf("[INFO] Response: %#v", oauthResp)
|
|
|
|
// Cleans up the existing keys before adding new ones
|
|
if len(oauthResp.AccessToken) > 0 {
|
|
newauth := []AuthenticationStore{}
|
|
for _, item := range appAuth.Fields {
|
|
if item.Key == "access_token" {
|
|
continue
|
|
}
|
|
|
|
newauth = append(newauth, item)
|
|
}
|
|
|
|
newauth = append(newauth, AuthenticationStore{
|
|
Key: "access_token",
|
|
Value: oauthResp.AccessToken,
|
|
})
|
|
|
|
appAuth.Fields = newauth
|
|
}
|
|
|
|
/*
|
|
if len(oauthResp.RefreshToken) > 0 {
|
|
//log.Printf("[DEBUG] Got NEW refresh token %s", oauthResp.RefreshToken)
|
|
|
|
newauth := []AuthenticationStore{}
|
|
for _, item := range appAuth.Fields {
|
|
if item.Key == "refresh_token" {
|
|
continue
|
|
}
|
|
|
|
newauth = append(newauth, item)
|
|
}
|
|
|
|
// Tested March 2024. Works to hotswap refresh tokens
|
|
// 4. M.C515_BL2.0.U.-Cot3MTbxsV8lXPwxLHd8Q1g1p49Mm31MamCfxBEHhXX1tGq2IDFBQ24dcX2RjC*cJW0Qdah9rO*2cEximZVVH0lBgjSEQckYrpv*9h1k1TWQCxmdatJGYjYxMVnflUtEL*dykvv4wEVvV2cdk!vSNih7BATGKrLoqB4ix38ufUjR4ynJxUcJS2hnIntqUPVHOsvXkFHncxDARAIrp7ZnvtXzR9gydhb*FkI!GaF8OIQwJgjqa7p0x8yhyJYLY0k1aAdFg8ehVsK6MzMVLB*dFQTBFzUdnF0tF09xAwsBbL0aWITXIEF*cPC5ghY07n!5H1Q8eOdcc*qOAFMQ!ov0wejM4eddXl*pytEt91IXC3b2
|
|
*/
|
|
|
|
if len(oauthResp.RefreshToken) > 0 {
|
|
appAuth.Fields = append(appAuth.Fields, AuthenticationStore{
|
|
Key: "refresh_token",
|
|
Value: oauthResp.RefreshToken,
|
|
})
|
|
|
|
//appAuth.Fields = newauth
|
|
}
|
|
|
|
if len(oauthUrl) > 0 {
|
|
// Check if url already exists with a good value
|
|
validUrl := false
|
|
for _, item := range appAuth.Fields {
|
|
if item.Key == "url" && len(item.Value) > 0 {
|
|
if strings.Contains(item.Value, "https://") || strings.Contains(item.Value, "http://") {
|
|
validUrl = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if !validUrl {
|
|
log.Printf("\n\n[DEBUG] Appending Oauth2 API URL %s\n\n", oauthUrl)
|
|
|
|
newAuth := []AuthenticationStore{}
|
|
for _, item := range appAuth.Fields {
|
|
if item.Key == "url" || item.Key == "expiration" {
|
|
continue
|
|
}
|
|
|
|
newAuth = append(newAuth, item)
|
|
}
|
|
|
|
appAuth.Fields = newAuth
|
|
appAuth.Fields = append(appAuth.Fields, AuthenticationStore{
|
|
Key: "url",
|
|
Value: oauthUrl,
|
|
})
|
|
}
|
|
} else {
|
|
log.Printf("[DEBUG] No app API URL to attach to Oauth2 auth?")
|
|
}
|
|
|
|
// FIXME: Does this work with string?
|
|
//https://stackoverflow.com/questions/43870554/microsoft-oauth2-authentication-not-returning-refresh-token
|
|
parsedTime := strconv.FormatInt(int64(time.Now().Unix())+int64(oauthResp.ExpiresIn), 10)
|
|
if oauthResp.ExpiresIn > 0 {
|
|
newauth := []AuthenticationStore{}
|
|
for _, item := range appAuth.Fields {
|
|
if item.Key == "expiration" {
|
|
continue
|
|
}
|
|
|
|
newauth = append(newauth, item)
|
|
}
|
|
|
|
newauth = append(newauth, AuthenticationStore{
|
|
Key: "expiration",
|
|
Value: parsedTime,
|
|
})
|
|
|
|
appAuth.Fields = newauth
|
|
}
|
|
|
|
if len(refreshUrl) > 0 && !refresh {
|
|
log.Printf("[DEBUG] Appending Oauth2 Refresh URL %s", refreshUrl)
|
|
appAuth.Fields = append(appAuth.Fields, AuthenticationStore{
|
|
Key: "refresh_url",
|
|
Value: refreshUrl,
|
|
})
|
|
//} else {
|
|
//log.Printf("[DEBUG] No refresh URL to attach to Oauth2 auth?")
|
|
}
|
|
|
|
// FIXME: Set up auth for this with oauth2 in app?
|
|
// How does this work with the SDK?
|
|
appAuth.OrgId = user.ActiveOrg.Id
|
|
appAuth.Defined = true
|
|
appAuth.Active = true
|
|
err = SetWorkflowAppAuthDatastore(ctx, appAuth, appAuth.Id)
|
|
if err != nil {
|
|
log.Printf("[WARNING] Failed setting up app auth %s for refresh: %s (oauth2)", appAuth.Id, err)
|
|
return appAuth, err
|
|
}
|
|
|
|
//log.Printf("%#v", oauthResp)
|
|
return appAuth, nil
|
|
}
|
|
|
|
/*
|
|
func fetchWellKnownConfig(ctx context.Context, issuer string, openIdAuthUrl string) (map[string]interface{}, error) {
|
|
// Clean issuer URL and construct well-known endpoint
|
|
issuer = strings.TrimSuffix(issuer, "/")
|
|
wellKnownURL := issuer + "/.well-known/openid-configuration"
|
|
|
|
// trying to check for keyclock edgecases
|
|
if len(openIdAuthUrl) > 0 && openIdAuthUrl != "none" {
|
|
openIdAuthUrl = strings.TrimSuffix(openIdAuthUrl, "/")
|
|
if idx := strings.Index(openIdAuthUrl, "/realms/"); idx != -1 {
|
|
realmStart := idx + len("/realms/")
|
|
realmEnd := strings.Index(openIdAuthUrl[realmStart:], "/")
|
|
if realmEnd != -1 {
|
|
realmBase := openIdAuthUrl[:realmStart+realmEnd]
|
|
wellKnownURL = realmBase + "/.well-known/openid-configuration"
|
|
}
|
|
}
|
|
}
|
|
|
|
resp, err := http.Get(wellKnownURL)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to fetch well-known config from %s: %w", wellKnownURL, err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != 200 {
|
|
return nil, fmt.Errorf("well-known endpoint returned status %d: %s", resp.StatusCode, wellKnownURL)
|
|
}
|
|
|
|
var config map[string]interface{}
|
|
if err := json.NewDecoder(resp.Body).Decode(&config); err != nil {
|
|
return nil, fmt.Errorf("failed to decode well-known config: %w, %s", err, wellKnownURL)
|
|
}
|
|
|
|
return config, nil
|
|
}
|
|
|
|
// VerifyIdTokenWithOIDC verifies an ID token using the go-oidc library and extracts claims
|
|
// This performs proper signature verification via JWKS, expiry check, issuer and audience validation
|
|
func VerifyIdTokenWithOIDC(ctx context.Context, idToken string, issuer string, clientID string) (*OpenidUserinfo, error) {
|
|
if idToken == "" {
|
|
return nil, fmt.Errorf("id token is empty")
|
|
}
|
|
if issuer == "" {
|
|
return nil, fmt.Errorf("issuer is empty")
|
|
}
|
|
if clientID == "" {
|
|
return nil, fmt.Errorf("client ID is empty")
|
|
}
|
|
|
|
// Create OIDC provider (fetches JWKS automatically from .well-known/openid-configuration)
|
|
provider, err := oidc.NewProvider(ctx, issuer)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create OIDC provider for issuer %s: %w", issuer, err)
|
|
}
|
|
|
|
// Create verifier with expected audience (client_id)
|
|
verifier := provider.Verifier(&oidc.Config{
|
|
ClientID: clientID,
|
|
})
|
|
|
|
// Verify the token (signature, expiry, issuer, audience)
|
|
token, err := verifier.Verify(ctx, idToken)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to verify ID token: %w", err)
|
|
}
|
|
|
|
// Extract claims
|
|
var claims OpenidUserinfo
|
|
if err := token.Claims(&claims); err != nil {
|
|
return nil, fmt.Errorf("failed to extract claims from ID token: %w", err)
|
|
}
|
|
|
|
// Set sub from the verified token
|
|
claims.Sub = token.Subject
|
|
|
|
return &claims, nil
|
|
}
|
|
|
|
// ExtractRolesFromIdToken verifies an ID token and extracts roles from various claim formats
|
|
// Returns a deduplicated list of roles from: roles, groups, realm_access.roles (Keycloak)
|
|
func ExtractRolesFromIdToken(ctx context.Context, idToken string, issuer string, clientID string) ([]string, error) {
|
|
claims, err := VerifyIdTokenWithOIDC(ctx, idToken, issuer, clientID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Collect roles from all possible sources
|
|
roleSet := make(map[string]bool)
|
|
|
|
for _, role := range claims.Roles {
|
|
roleSet[role] = true
|
|
}
|
|
for _, group := range claims.Groups {
|
|
roleSet[group] = true
|
|
}
|
|
for _, role := range claims.RealmAccess.Roles {
|
|
roleSet[role] = true
|
|
}
|
|
|
|
// Convert to slice
|
|
roles := make([]string, 0, len(roleSet))
|
|
for role := range roleSet {
|
|
roles = append(roles, role)
|
|
}
|
|
|
|
return roles, nil
|
|
}
|
|
*/
|
|
|
|
func VerifyIdToken(ctx context.Context, idToken string) (IdTokenCheck, error) {
|
|
// Check org in nonce -> check if ID points back to an org
|
|
outerSplit := strings.Split(string(idToken), ".")
|
|
for _, innerstate := range outerSplit {
|
|
log.Printf("[DEBUG] OpenID STATE (temporary): %s", innerstate)
|
|
decoded, err := base64.StdEncoding.DecodeString(innerstate)
|
|
if err != nil {
|
|
log.Printf("[DEBUG] Failed base64 decode of state (1): %s", err)
|
|
|
|
// Random padding problems
|
|
innerstate += "="
|
|
decoded, err = base64.StdEncoding.DecodeString(innerstate)
|
|
if err != nil {
|
|
log.Printf("[DEBUG] Failed base64 decode of state (2): %s", err)
|
|
|
|
// Double padding problem fix lol (this actually works)
|
|
innerstate += "="
|
|
decoded, err = base64.StdEncoding.DecodeString(innerstate)
|
|
if err != nil {
|
|
log.Printf("[ERROR] Failed base64 decode of state (3): %s", err)
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
var token IdTokenCheck
|
|
err = json.Unmarshal([]byte(decoded), &token)
|
|
if err != nil {
|
|
log.Printf("[INFO] IDToken unmarshal error: %s", err)
|
|
continue
|
|
}
|
|
|
|
// Aud = client secret
|
|
// Nonce = contains all the info
|
|
if len(token.Aud) <= 0 {
|
|
log.Printf("[WARNING] Couldn't find AUD in JSON (required) - continuing to check. Current: %s", string(decoded))
|
|
continue
|
|
}
|
|
|
|
if len(token.Nonce) > 0 {
|
|
parsedState, err := base64.StdEncoding.DecodeString(token.Nonce)
|
|
if err != nil {
|
|
log.Printf("[ERROR] Failed state split: %s", err)
|
|
}
|
|
|
|
foundOrg := ""
|
|
foundChallenge := ""
|
|
stateSplit := strings.Split(string(parsedState), "&")
|
|
regexPattern := `EXTRA string=([A-Za-z0-9~.]+)`
|
|
re := regexp.MustCompile(regexPattern)
|
|
for _, innerstate := range stateSplit {
|
|
itemsplit := strings.SplitN(innerstate, "=", 2)
|
|
if len(itemsplit) <= 1 {
|
|
log.Printf("[WARNING] No key:value: %s", innerstate)
|
|
continue
|
|
}
|
|
|
|
key := strings.TrimSpace(itemsplit[0])
|
|
value := strings.TrimSpace(itemsplit[1])
|
|
if itemsplit[0] == "org" {
|
|
foundOrg = value
|
|
}
|
|
|
|
if key == "challenge" {
|
|
// Extract the "extra string" value from the challenge value
|
|
matches := re.FindStringSubmatch(value)
|
|
if len(matches) > 1 {
|
|
extractedString := matches[1]
|
|
foundChallenge = extractedString
|
|
log.Printf("Extracted 'extra string' value is: %s", extractedString)
|
|
} else {
|
|
foundChallenge = strings.TrimSpace(itemsplit[1])
|
|
log.Printf("No 'extra string' value found in challenge: %s", value)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(foundOrg) == 0 {
|
|
log.Printf("[ERROR] No org specified in state (2)")
|
|
return IdTokenCheck{}, err
|
|
}
|
|
org, err := GetOrg(ctx, foundOrg)
|
|
if err != nil {
|
|
log.Printf("[WARNING] Error getting org in OpenID (2): %s", err)
|
|
return IdTokenCheck{}, err
|
|
}
|
|
// Validating the user itself
|
|
if token.Aud == org.SSOConfig.OpenIdClientId || foundChallenge == org.SSOConfig.OpenIdClientSecret {
|
|
log.Printf("[DEBUG] Correct token aud & challenge - successful login!")
|
|
token.Org = *org
|
|
return token, nil
|
|
} else {
|
|
}
|
|
}
|
|
}
|
|
|
|
return IdTokenCheck{}, errors.New("Couldn't verify nonce")
|
|
}
|
|
|
|
func IsRunningInCluster() bool {
|
|
_, existsHost := os.LookupEnv("KUBERNETES_SERVICE_HOST")
|
|
_, existsPort := os.LookupEnv("KUBERNETES_SERVICE_PORT")
|
|
return existsHost && existsPort
|
|
}
|
|
|
|
func GetPodName() string {
|
|
if len(os.Getenv("MY_POD_NAME")) > 0 {
|
|
return os.Getenv("MY_POD_NAME")
|
|
}
|
|
|
|
log.Printf("[DEBUG] No podname found to attach to")
|
|
|
|
return ""
|
|
}
|
|
|
|
func GetKubernetesNamespace() (string, error) {
|
|
namespaceFile := "/var/run/secrets/kubernetes.io/serviceaccount/namespace"
|
|
|
|
namespaceFilepathEnv := os.Getenv("KUBERNETES_NAMESPACE_FILEPATH")
|
|
if namespaceFilepathEnv != "" {
|
|
namespaceFile = namespaceFilepathEnv
|
|
}
|
|
|
|
file, err := os.Open(namespaceFile)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
defer file.Close()
|
|
scanner := bufio.NewScanner(file)
|
|
if scanner.Scan() {
|
|
return scanner.Text(), nil
|
|
}
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return "", fmt.Errorf("namespace file is empty")
|
|
}
|
|
|
|
func GetKubernetesClient() (*kubernetes.Clientset, *rest.Config, error) {
|
|
|
|
config := &rest.Config{}
|
|
var err error
|
|
|
|
/*
|
|
// Not in use for now. This is a in-cluster override from orborus
|
|
kubeconfigContent := os.Getenv("KUBERNETES_CONFIG")
|
|
if len(kubeconfigContent) > 0 {
|
|
log.Printf("[INFO] Using KUBERNETES_CONFIG to set up Kubernetes client: %#v", os.Getenv("KUBERNETES_CONFIG"))
|
|
config, err := rest.InClusterConfig()
|
|
if err != nil {
|
|
log.Printf("[ERROR] Failed to create Kubernetes client from in-cluster config: %s", err)
|
|
} else {
|
|
// Replace client configuration with kubeconfig content
|
|
config, err = clientcmd.RESTConfigFromKubeConfig([]byte(kubeconfigContent))
|
|
if err != nil {
|
|
log.Printf("[ERROR] Failed to create Kubernetes client from KUBERNETES_CONFIG: %s", err)
|
|
} else {
|
|
// Create Kubernetes client
|
|
clientset, err := kubernetes.NewForConfig(config)
|
|
if err != nil {
|
|
return nil, config, err
|
|
}
|
|
|
|
return clientset, config, nil
|
|
}
|
|
}
|
|
}
|
|
*/
|
|
|
|
// Look for the kubernetes serviceaccount path /var/run/secrets/kubernetes.io/serviceaccount
|
|
// If it exists, use it to create the client
|
|
// /var/run/secrets/kubernetes.io/serviceaccount
|
|
path := "/var/run/secrets/kubernetes.io/serviceaccount"
|
|
if _, err := os.Stat(path); err == nil {
|
|
//log.Printf("[DEBUG] Using service account filepath to create kubernetes client")
|
|
config, err = rest.InClusterConfig()
|
|
if err != nil {
|
|
return nil, config, err
|
|
}
|
|
|
|
clientset, err := kubernetes.NewForConfig(config)
|
|
if err != nil {
|
|
return nil, config, err
|
|
}
|
|
|
|
return clientset, config, nil
|
|
}
|
|
|
|
if IsRunningInCluster() {
|
|
config, err := rest.InClusterConfig()
|
|
if err != nil {
|
|
return nil, config, err
|
|
}
|
|
|
|
clientset, err := kubernetes.NewForConfig(config)
|
|
if err != nil {
|
|
return nil, config, err
|
|
}
|
|
|
|
return clientset, config, nil
|
|
}
|
|
|
|
home := homedir.HomeDir()
|
|
kubeconfigPath := filepath.Join(home, ".kube", "config")
|
|
config, err = clientcmd.BuildConfigFromFlags("", kubeconfigPath)
|
|
if err != nil {
|
|
return nil, config, err
|
|
}
|
|
|
|
clientset, err := kubernetes.NewForConfig(config)
|
|
if err != nil {
|
|
return nil, config, err
|
|
}
|
|
|
|
return clientset, config, nil
|
|
}
|
|
|
|
func GetCurrentPodNetworkConfig(ctx context.Context, clientset *kubernetes.Clientset, namespace, podName string) (*corev1.PodStatus, error) {
|
|
pod, err := clientset.CoreV1().Pods(namespace).Get(ctx, podName, metav1.GetOptions{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &pod.Status, nil
|
|
}
|