package profile import ( "encoding/json" "fmt" "io" "net" "net/http" "time" "edge-infra.dev/pkg/edge/iam/config" "edge-infra.dev/pkg/edge/iam/util" ) type Profile struct { Subject string `json:"sub"` Organization string `json:"org"` // Site string `json:"eu"` // todo: add Roles string `json:"rls"` GivenName string `json:"given_name,omitempty"` FamilyName string `json:"family_name,omitempty"` FullName string `json:"name,omitempty"` Email string `json:"email,omitempty"` Address *AddressClaim `json:"address,omitempty"` LastUpdated int64 `json:"last_updated"` Alias string `json:"alias"` DeviceLogin string `json:"device_login,omitempty"` Age int `json:"age,omitempty"` } type Address struct { City string `json:"city,omitempty"` Country string `json:"country,omitempty"` Street string `json:"street,omitempty"` State string `json:"state,omitempty"` PostalCode string `json:"postalCode,omitempty"` } type ProvisioningUserProfileResponse struct { GivenName string `json:"givenName,omitempty"` FamilyName string `json:"familyName,omitempty"` FullName string `json:"fullName,omitempty"` Email string `json:"email,omitempty"` Address *Address `json:"address,omitempty"` } func (p *Profile) RequireVerification(isOffline bool) (bool, error) { lastUpdated := time.Unix(p.LastUpdated, 0) duration := time.Since(lastUpdated) // no verification needed if duration < config.GetProfileLifespan() { return false, nil } // skip verification during LAN outage if isOffline { return false, nil } // skip verification if can't reach bsl/okta (WAN outage) client := &http.Client{ Timeout: config.CloudIDPTimeout(), } url := config.BslSecurityURL() if config.OktaEnabled() { url = config.OktaIssuer() } resp, err := client.Get(url) // we timed-out, we're OK with skipping verification if _, ok := err.(net.Error); ok { return false, nil } // something else happened if err != nil { return true, err } defer resp.Body.Close() return true, nil } // GetUserProfile from the provisioning user-profiles service func GetUserProfile(accessToken string) (*ProvisioningUserProfileResponse, error) { url := config.ProvisioningUserProfilesURL() req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, err } // set required headers req.Header.Set("nep-organization", config.OrganizationName()) req.Header.Set("Authorization", "AccessToken "+accessToken) client := &http.Client{} resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { respBody, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read user profile response body. %s", err.Error()) } return nil, fmt.Errorf("failed to get user profile. response body: %s", string(respBody)) } body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } var userProfileResponse ProvisioningUserProfileResponse err = json.Unmarshal(body, &userProfileResponse) if err != nil { return nil, err } return &userProfileResponse, nil } // grabs BSL Roles and assigns them to a profile // accessToken (string): an Okta access token // nepOrg (string) nep-organization value // returns roles, error func GetRoles(nepOrg string, envURL string, accessToken string) (string, error) { url := envURL req, err := http.NewRequest("GET", url, nil) if err != nil { return "", err } // set required headers req.Header.Set("Authorization", "AccessToken "+accessToken) req.Header.Set("nep-organization", nepOrg) client := &http.Client{} resp, err := client.Do(req) if err != nil { return "", err } defer resp.Body.Close() // if not 200/201 to get BSL /introspect, return error if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { respBody, err := io.ReadAll(resp.Body) if err != nil { return "", fmt.Errorf("Unexpected status code: %d. Error reading response body: %s", resp.StatusCode, err.Error()) } return "", fmt.Errorf("Unexpected status code: %d. Response body: %s", resp.StatusCode, string(respBody)) } body, err := io.ReadAll(resp.Body) if err != nil { return "", err } var data map[string]interface{} err = json.Unmarshal(body, &data) if err != nil { return "", err } // get roles as []interface{} grantedRoles, ok := data["grantedRoles"].([]interface{}) if !ok { return "", err } // convert []interfaces{} to an array of strings output := []string{} for _, item := range grantedRoles { roleName, ok := item.(map[string]interface{})["roleName"].(string) if ok { output = append(output, roleName) } } // serialize to make one big string roles, err := util.Serialize(output) if err != nil { return "", err } return roles, nil }