Skip to content

Commit

Permalink
Merge pull request #26 from watson-developer-cloud/load-creds
Browse files Browse the repository at this point in the history
feat(Credential Initialization): Load credentials from file
  • Loading branch information
ehdsouza authored Feb 1, 2019
2 parents d787122 + 2818929 commit 19d0f25
Show file tree
Hide file tree
Showing 17 changed files with 175 additions and 28 deletions.
44 changes: 41 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,47 @@ Watson services are migrating to token-based Identity and Access Management (IAM
### Getting credentials
To find out which authentication to use, view the service credentials. You find the service credentials for authentication the same way for all Watson services:

1. Go to the IBM Cloud [Dashboard](https://console.bluemix.net/dashboard/apps?category=ai) page.
1. Either click an existing Watson service instance or click [**Create resource > AI**](https://console.bluemix.net/catalog/?category=ai) and create a service instance.
1. Copy the `url` and either `apikey` or `username` and `password`. Click **Show** if the credentials are masked.
1. Go to the IBM Cloud [Dashboard](https://cloud.ibm.com/) page.
1. Either click an existing Watson service instance in your [resource list](https://cloud.ibm.com/resources) or click [**Create resource > AI**](https://cloud.ibm.com/catalog?category=ai) and create a service instance.
1. Click on the **Manage** item in the left nav bar of your service instance.

On this page, you should be able to see your credentials for accessing your service instance.

### Supplying credentials

There are two ways to supply the credentials you found above to the SDK for authentication.

#### Credential file (easier!)

With a credential file, you just need to put the file in the right place and the SDK will do the work of parsing and authenticating. You can get this file by clicking the **Download** button for the credentials in the **Manage** tab of your service instance.

The file downloaded will be called `ibm-credentials.env`. This is the name the SDK will search for and **must** be preserved unless you want to configure the file path (more on that later). The SDK will look for your `ibm-credentials.env` file in the following places (in order):

- Your system's home directory
- The top-level directory of the project you're using the SDK in

As long as you set that up correctly, you don't have to worry about setting any authentication options in your code. So, for example, if you created and downloaded the credential file for your Discovery instance, you just need to do the following:

```go
discovery, discoveryErr := NewDiscoveryV1(&DiscoveryV1Options{
Version: "2018-03-05",
})
```

And that's it!

If you're using more than one service at a time in your code and get two different `ibm-credentials.env` files, just put the contents together in one `ibm-credentials.env` file and the SDK will handle assigning credentials to their appropriate services.

If you would like to configure the location/name of your credential file, you can set an environment variable called `IBM_CREDENTIALS_FILE`. **This will take precedence over the locations specified above.** Here's how you can do that:

```bash
export IBM_CREDENTIALS_FILE="<path>"
```

where `<path>` is something like `/home/user/Downloads/<file_name>.env`.

#### Manually
If you'd prefer to set authentication values manually in your code, the SDK supports that as well. The way you'll do this depends on what type of credentials your service instance gives you.

### IAM
IBM Cloud is migrating to token-based Identity and Access Management (IAM) authentication. IAM authentication uses a service API key to get an access token that is passed with the call. Access tokens are valid for approximately one hour and must be regenerated.
Expand Down
2 changes: 1 addition & 1 deletion assistantv1/assistant_v1.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func NewAssistantV1(options *AssistantV1Options) (*AssistantV1, error) {
IAMAccessToken: options.IAMAccessToken,
IAMURL: options.IAMURL,
}
service, serviceErr := core.NewWatsonService(serviceOptions, "conversation")
service, serviceErr := core.NewWatsonService(serviceOptions, "conversation", "Assistant")
if serviceErr != nil {
return nil, serviceErr
}
Expand Down
2 changes: 1 addition & 1 deletion assistantv2/assistant_v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func NewAssistantV2(options *AssistantV2Options) (*AssistantV2, error) {
IAMAccessToken: options.IAMAccessToken,
IAMURL: options.IAMURL,
}
service, serviceErr := core.NewWatsonService(serviceOptions, "conversation")
service, serviceErr := core.NewWatsonService(serviceOptions, "conversation", "Assistant")
if serviceErr != nil {
return nil, serviceErr
}
Expand Down
2 changes: 1 addition & 1 deletion comparecomplyv1/compare_comply_v1.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ func NewCompareComplyV1(options *CompareComplyV1Options) (*CompareComplyV1, erro
IAMAccessToken: options.IAMAccessToken,
IAMURL: options.IAMURL,
}
service, serviceErr := core.NewWatsonService(serviceOptions, "compare-comply")
service, serviceErr := core.NewWatsonService(serviceOptions, "compare-comply", "Compare Comply")
if serviceErr != nil {
return nil, serviceErr
}
Expand Down
14 changes: 14 additions & 0 deletions core/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import (
"encoding/json"
"errors"
"fmt"
"os"
"reflect"
"regexp"
"runtime"
"strings"

validator "gopkg.in/go-playground/validator.v9"
Expand Down Expand Up @@ -139,3 +141,15 @@ func HasBadFirstOrLastChar(str string) bool {
return strings.HasPrefix(str, "{") || strings.HasPrefix(str, "\"") ||
strings.HasSuffix(str, "}") || strings.HasSuffix(str, "\"")
}

// UserHomeDir returns the user home directory
func UserHomeDir() string {
if runtime.GOOS == "windows" {
home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
if home == "" {
home = os.Getenv("USERPROFILE")
}
return home
}
return os.Getenv("HOME")
}
91 changes: 83 additions & 8 deletions core/watson.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package core

import (
"bufio"
"bytes"
"crypto/tls"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"path"
"runtime"
"strings"
"time"
Expand All @@ -16,11 +19,18 @@ import (

// common constants for core
const (
APIKey = "apikey"
ICPPrefix = "icp-"
UserAgent = "User-Agent"
Authorization = "Authorization"
Bearer = "Bearer"
APIKey = "apikey"
ICPPrefix = "icp-"
UserAgent = "User-Agent"
Authorization = "Authorization"
Bearer = "Bearer"
IBM_CREDENTIAL_FILE_ENV = "IBM_CREDENTIALS_FILE"
DEFAULT_CREDENTIAL_FILE_NAME = "ibm-credentials.env"
URL = "url"
USERNAME = "username"
PASSWORD = "password"
IAM_API_KEY = "iam_apikey"
IAM_URL = "iam_url"
)

// ServiceOptions Service options
Expand All @@ -44,7 +54,7 @@ type WatsonService struct {
}

// NewWatsonService Instantiate a Watson Service
func NewWatsonService(options *ServiceOptions, serviceName string) (*WatsonService, error) {
func NewWatsonService(options *ServiceOptions, serviceName, displayName string) (*WatsonService, error) {
if HasBadFirstOrLastChar(options.URL) {
return nil, fmt.Errorf("The URL shouldn't start or end with curly brackets or quotes. Be sure to remove any {} and \" characters surrounding your URL")
}
Expand All @@ -61,6 +71,7 @@ func NewWatsonService(options *ServiceOptions, serviceName string) (*WatsonServi
userAgent += "-" + runtime.GOOS
service.UserAgent = userAgent

// 1. Credentials are passed in constructor
if options.Username != "" && options.Password != "" {
if options.Username == APIKey && !strings.HasPrefix(options.Password, ICPPrefix) {
if err := service.SetTokenManager(options.IAMApiKey, options.IAMAccessToken, options.IAMURL); err != nil {
Expand All @@ -75,8 +86,16 @@ func NewWatsonService(options *ServiceOptions, serviceName string) (*WatsonServi
if err := service.SetTokenManager(options.IAMApiKey, options.IAMAccessToken, options.IAMURL); err != nil {
return nil, err
}
} else {
// Try accessing VCAP_SERVICES env variable
}

// 2. Credentials from credential file
if displayName != "" && service.Options.Username == "" && service.TokenManager == nil {
serviceName := strings.ToLower(strings.Replace(displayName, " ", "_", -1))
service.loadFromCredentialFile(serviceName, "=")
}

// 3. Try accessing VCAP_SERVICES env variable
if service.Options.Username == "" && service.TokenManager == nil {
err := service.accessVCAP(serviceName)
if err != nil {
return nil, err
Expand Down Expand Up @@ -246,3 +265,59 @@ func (service *WatsonService) accessVCAP(serviceName string) error {

return fmt.Errorf("you must specify an IAM API key or username and password service credentials")
}

func (service *WatsonService) loadFromCredentialFile(serviceName string, separator string) error {
// File path specified by env variable
credentialFilePath := os.Getenv(IBM_CREDENTIAL_FILE_ENV)

// Home directory
if credentialFilePath == "" {
var filePath = path.Join(UserHomeDir(), DEFAULT_CREDENTIAL_FILE_NAME)
if _, err := os.Stat(filePath); err == nil {
credentialFilePath = filePath
}
}

// Top-level of project directory
if credentialFilePath == "" {
dir, _ := os.Getwd()
var filePath = path.Join(dir, "..", DEFAULT_CREDENTIAL_FILE_NAME)
if _, err := os.Stat(filePath); err == nil {
credentialFilePath = filePath
}
}

if credentialFilePath != "" {
file, err := os.Open(credentialFilePath)
if err != nil {
return err
}
defer file.Close()

scanner := bufio.NewScanner(file)
for scanner.Scan() {
var line = scanner.Text()
var keyVal = strings.Split(strings.ToLower(line), separator)
if len(keyVal) == 2 {
service.setCredentialBasedOnType(serviceName, keyVal[0], keyVal[1])
}
}
}
return nil
}

func (service *WatsonService) setCredentialBasedOnType(serviceName, key, value string) {
if strings.Contains(key, serviceName) {
if strings.Contains(key, APIKey) {
service.SetIAMAPIKey(value)
} else if strings.Contains(key, URL) {
service.SetURL(value)
} else if strings.Contains(key, USERNAME) {
service.Options.Username = value
} else if strings.Contains(key, PASSWORD) {
service.Options.Password = value
} else if strings.Contains(key, IAM_API_KEY) {
service.SetIAMAPIKey(value)
}
}
}
26 changes: 21 additions & 5 deletions core/watson_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"fmt"
"net/http"
"net/http/httptest"
"os"
"path"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -32,7 +34,7 @@ func TestRequestResponseAsJSON(t *testing.T) {
Username: "xxx",
Password: "yyy",
}
service, _ := NewWatsonService(options, "watson")
service, _ := NewWatsonService(options, "watson", "watson")
detailedResponse, _ := service.Request(req, new(Foo))
assert.Equal(t, "wonder woman", *detailedResponse.Result.(*Foo).Name)
}
Expand All @@ -43,7 +45,7 @@ func TestIncorrectCreds(t *testing.T) {
Username: "{yyy}",
Password: "zzz",
}
_, serviceErr := NewWatsonService(options, "watson")
_, serviceErr := NewWatsonService(options, "watson", "watson")
assert.Equal(t, "The username shouldn't start or end with curly brackets or quotes. Be sure to remove any {} and \" characters surrounding your username", serviceErr.Error())
}

Expand All @@ -53,7 +55,7 @@ func TestIncorrectURL(t *testing.T) {
Username: "yyy",
Password: "zzz",
}
_, serviceErr := NewWatsonService(options, "watson")
_, serviceErr := NewWatsonService(options, "watson", "watson")
assert.Equal(t, "The URL shouldn't start or end with curly brackets or quotes. Be sure to remove any {} and \" characters surrounding your URL", serviceErr.Error())
}

Expand All @@ -63,7 +65,7 @@ func TestDisableSSLverification(t *testing.T) {
Username: "xxx",
Password: "yyy",
}
service, _ := NewWatsonService(options, "watson")
service, _ := NewWatsonService(options, "watson", "watson")
assert.Nil(t, service.Client.Transport)
service.DisableSSLVerification()
assert.NotNil(t, service.Client.Transport)
Expand All @@ -87,7 +89,21 @@ func TestAuthentication(t *testing.T) {
Username: "xxx",
Password: "yyy",
}
service, _ := NewWatsonService(options, "watson")
service, _ := NewWatsonService(options, "watson", "watson")

service.Request(req, new(Foo))
}

func TestLoadingFromCredentialFile(t *testing.T) {
pwd, _ := os.Getwd()
credentialFilePath := path.Join(pwd, "/../resources/ibm-credentials.env")
os.Setenv("IBM_CREDENTIALS_FILE", credentialFilePath)
options := &ServiceOptions{}
service, _ := NewWatsonService(options, "watson", "watson")
assert.Equal(t, service.Options.IAMApiKey, "5678efgh")
os.Unsetenv("IBM_CREDENTIALS_FILE")

options2 := &ServiceOptions{IAMApiKey: "xxx"}
service2, _ := NewWatsonService(options2, "watson", "watson")
assert.Equal(t, service2.Options.IAMApiKey, "xxx")
}
2 changes: 1 addition & 1 deletion discoveryv1/discovery_v1.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func NewDiscoveryV1(options *DiscoveryV1Options) (*DiscoveryV1, error) {
IAMAccessToken: options.IAMAccessToken,
IAMURL: options.IAMURL,
}
service, serviceErr := core.NewWatsonService(serviceOptions, "discovery")
service, serviceErr := core.NewWatsonService(serviceOptions, "discovery", "Discovery")
if serviceErr != nil {
return nil, serviceErr
}
Expand Down
2 changes: 1 addition & 1 deletion languagetranslatorv3/language_translator_v3.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func NewLanguageTranslatorV3(options *LanguageTranslatorV3Options) (*LanguageTra
IAMAccessToken: options.IAMAccessToken,
IAMURL: options.IAMURL,
}
service, serviceErr := core.NewWatsonService(serviceOptions, "language_translator")
service, serviceErr := core.NewWatsonService(serviceOptions, "language_translator", "Language Translator")
if serviceErr != nil {
return nil, serviceErr
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func NewNaturalLanguageClassifierV1(options *NaturalLanguageClassifierV1Options)
IAMAccessToken: options.IAMAccessToken,
IAMURL: options.IAMURL,
}
service, serviceErr := core.NewWatsonService(serviceOptions, "natural_language_classifier")
service, serviceErr := core.NewWatsonService(serviceOptions, "natural_language_classifier", "Natural Language Classifier")
if serviceErr != nil {
return nil, serviceErr
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func NewNaturalLanguageUnderstandingV1(options *NaturalLanguageUnderstandingV1Op
IAMAccessToken: options.IAMAccessToken,
IAMURL: options.IAMURL,
}
service, serviceErr := core.NewWatsonService(serviceOptions, "natural-language-understanding")
service, serviceErr := core.NewWatsonService(serviceOptions, "natural-language-understanding", "Natural Language Understanding")
if serviceErr != nil {
return nil, serviceErr
}
Expand Down
2 changes: 1 addition & 1 deletion personalityinsightsv3/personality_insights_v3.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ func NewPersonalityInsightsV3(options *PersonalityInsightsV3Options) (*Personali
IAMAccessToken: options.IAMAccessToken,
IAMURL: options.IAMURL,
}
service, serviceErr := core.NewWatsonService(serviceOptions, "personality_insights")
service, serviceErr := core.NewWatsonService(serviceOptions, "personality_insights", "Personality Insights")
if serviceErr != nil {
return nil, serviceErr
}
Expand Down
4 changes: 4 additions & 0 deletions resources/ibm-credentials.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
VISUAL_RECOGNITION_APIKEY=1234abcd
VISUAL_RECOGNITION_URL=https://stgwat-us-south-mzr-cruiser6.us-south.containers.mybluemix.net/visual-recognition/api
WATSON_APIKEY=5678efgh
WATSON_URL=https://gateway-s.watsonplatform.net/watson/api
2 changes: 1 addition & 1 deletion speechtotextv1/speech_to_text_v1.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func NewSpeechToTextV1(options *SpeechToTextV1Options) (*SpeechToTextV1, error)
IAMAccessToken: options.IAMAccessToken,
IAMURL: options.IAMURL,
}
service, serviceErr := core.NewWatsonService(serviceOptions, "speech_to_text")
service, serviceErr := core.NewWatsonService(serviceOptions, "speech_to_text", "Speech to Text")
if serviceErr != nil {
return nil, serviceErr
}
Expand Down
2 changes: 1 addition & 1 deletion texttospeechv1/text_to_speech_v1.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ func NewTextToSpeechV1(options *TextToSpeechV1Options) (*TextToSpeechV1, error)
IAMAccessToken: options.IAMAccessToken,
IAMURL: options.IAMURL,
}
service, serviceErr := core.NewWatsonService(serviceOptions, "text_to_speech")
service, serviceErr := core.NewWatsonService(serviceOptions, "text_to_speech", "Text to Speech")
if serviceErr != nil {
return nil, serviceErr
}
Expand Down
2 changes: 1 addition & 1 deletion toneanalyzerv3/tone_analyzer_v3.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func NewToneAnalyzerV3(options *ToneAnalyzerV3Options) (*ToneAnalyzerV3, error)
IAMAccessToken: options.IAMAccessToken,
IAMURL: options.IAMURL,
}
service, serviceErr := core.NewWatsonService(serviceOptions, "tone_analyzer")
service, serviceErr := core.NewWatsonService(serviceOptions, "tone_analyzer", "Tone Analyzer")
if serviceErr != nil {
return nil, serviceErr
}
Expand Down
2 changes: 1 addition & 1 deletion visualrecognitionv3/visual_recognition_v3.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func NewVisualRecognitionV3(options *VisualRecognitionV3Options) (*VisualRecogni
IAMAccessToken: options.IAMAccessToken,
IAMURL: options.IAMURL,
}
service, serviceErr := core.NewWatsonService(serviceOptions, "watson_vision_combined")
service, serviceErr := core.NewWatsonService(serviceOptions, "watson_vision_combined", "Visual Recognition")
if serviceErr != nil {
return nil, serviceErr
}
Expand Down

0 comments on commit 19d0f25

Please sign in to comment.