Skip to content

Commit

Permalink
Merge branch 'release/0.3.2'
Browse files Browse the repository at this point in the history
  • Loading branch information
gildas committed Jun 30, 2021
2 parents c0db124 + 4a3d9c6 commit b326061
Show file tree
Hide file tree
Showing 18 changed files with 334 additions and 66 deletions.
14 changes: 7 additions & 7 deletions access_token_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,12 @@ func TestCanResetGrantAccessToken(t *testing.T) {
ExpiresOn: time.Now().UTC().Add(2 * time.Hour),
}
client := purecloud.NewClient(&purecloud.ClientOptions{}).SetAuthorizationGrant(&purecloud.ClientCredentialsGrant{Token: token})
assert.Equal(t, "Bearer", client.AuthorizationGrant.AccessToken().Type)
assert.Equal(t, "Very Long String", client.AuthorizationGrant.AccessToken().Token)
assert.Equal(t, "Bearer", client.Grant.AccessToken().Type)
assert.Equal(t, "Very Long String", client.Grant.AccessToken().Token)

client.AuthorizationGrant.AccessToken().Reset()
assert.Empty(t, client.AuthorizationGrant.AccessToken().Token, "The Token string should be empty")
assert.Empty(t, client.AuthorizationGrant.AccessToken().Type, "The Token type should be empty")
assert.True(t, client.AuthorizationGrant.AccessToken().IsExpired(), "The Token should be expired")
assert.False(t, client.AuthorizationGrant.AccessToken().IsValid(), "The Token should not be valid")
client.Grant.AccessToken().Reset()
assert.Empty(t, client.Grant.AccessToken().Token, "The Token string should be empty")
assert.Empty(t, client.Grant.AccessToken().Type, "The Token type should be empty")
assert.True(t, client.Grant.AccessToken().IsExpired(), "The Token should be expired")
assert.False(t, client.Grant.AccessToken().IsValid(), "The Token should not be valid")
}
77 changes: 72 additions & 5 deletions auth.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,75 @@
package purecloud

// AuthorizationGrant describes the capabilities authorization grants must have
type AuthorizationGrant interface {
// Authorize this Grant with PureCloud
Authorize(client *Client) error
AccessToken() *AccessToken
import (
"github.com/gildas/go-core"
"github.com/google/uuid"
)

// Authorizer describes what a grants should do
type Authorizer interface {
Authorize(client *Client) error // Authorize a client with PureCloud
AccessToken() *AccessToken // Get the Access Token obtained by the Authorizer
core.Identifiable // Implements core.Identifiable
}

// AuthorizationSubject describes the roles and permissions of a Subject
type AuthorizationSubject struct {
ID uuid.UUID `json:"id"`
SelfUri string `json:"selfUri"`
Name string `json:"name"`
Grants []AuthorizationGrant `json:"grants"`
Version int `json:"version"`
}

type AuthorizationGrant struct {
SubjectID uuid.UUID `json:"subjectId"`
Division AuthorizationDivision `json:"division"`
Role AuthorizationGrantRole `json:"role"`
CreatedAt string `json:"grantMadeAt"` // TODO: this is an ISO8601 date
}

type AuthorizationDivision struct {
ID uuid.UUID `json:"id"`
SelfUri string `json:"selfUri"`
Name string `json:"name"`
Description string `json:"description"` // required
IsHome bool `json:"homeDivision"`
ObjectCounts map[string]int `json:"objectCounts"`
}

type AuthorizationGrantRole struct {
ID uuid.UUID `json:"id"`
SelfUri string `json:"selfUri"`
Name string `json:"name"`
Description string `json:"description"`
IsDefault bool `json:"default"`
Policies []AuthorizationGrantPolicy `json:"policies"`
}

type AuthorizationGrantPolicy struct {
EntityName string `json:"entityName"`
Domain string `json:"domain"`
Condition string `json:"condition"`
Actions []string `json:"actions"`
}

// GetID gets the identifier
//
// implements core.Identifiable
func (subject AuthorizationSubject) GetID() uuid.UUID {
return subject.ID
}

// GetID gets the identifier
//
// implements core.Identifiable
func (division AuthorizationDivision) GetID() uuid.UUID {
return division.ID
}

// GetID gets the identifier
//
// implements core.Identifiable
func (role AuthorizationGrantRole) GetID() uuid.UUID {
return role.ID
}
11 changes: 9 additions & 2 deletions auth_clientcredentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,28 @@ import (
//
// See: https://developer.mypurecloud.com/api/rest/authorization/use-client-credentials.html
type ClientCredentialsGrant struct {
ClientID uuid.UUID
ClientID uuid.UUID
Secret string
Token AccessToken
CustomData interface{}
TokenUpdated chan UpdatedAccessToken
}

// GetID gets the client Identifier
//
// Implements core.Identifiable
func (grant *ClientCredentialsGrant) GetID() uuid.UUID {
return grant.ClientID
}

// Authorize this Grant with PureCloud
func (grant *ClientCredentialsGrant) Authorize(client *Client) (err error) {
log := client.Logger.Child(nil, "authorize", "grant", "client_credentials")

log.Infof("Authenticating with %s using Client Credentials grant", client.Region)

// Validates the Grant
if len(grant.ClientID) == 0 {
if grant.ClientID == uuid.Nil {
return errors.ArgumentMissing.With("ClientID").WithStack()
}
if len(grant.Secret) == 0 {
Expand Down
14 changes: 11 additions & 3 deletions auth_code.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ import (

"github.com/gildas/go-errors"
"github.com/gildas/go-request"
"github.com/google/uuid"
)

// AuthorizationCodeGrant implements PureCloud's Client Authorization Code Grants
// See: https://developer.mypurecloud.com/api/rest/authorization/use-authorization-code.html
type AuthorizationCodeGrant struct {
ClientID string
ClientID uuid.UUID
Secret string
Code string
RedirectURL *url.URL
Expand All @@ -20,14 +21,21 @@ type AuthorizationCodeGrant struct {
TokenUpdated chan UpdatedAccessToken
}

// GetID gets the client Identifier
//
// Implements core.Identifiable
func (grant *AuthorizationCodeGrant) GetID() uuid.UUID {
return grant.ClientID
}

// Authorize this Grant with PureCloud
func (grant *AuthorizationCodeGrant) Authorize(client *Client) (err error) {
log := client.Logger.Child(nil, "authorize", "grant", "authorization_code")

log.Infof("Authenticating with %s using Authorization Code grant", client.Region)

// Validates the Grant
if len(grant.ClientID) == 0 {
if grant.ClientID == uuid.Nil {
return errors.ArgumentMissing.With("ClientID").WithStack()
}
if len(grant.Secret) == 0 {
Expand All @@ -49,7 +57,7 @@ func (grant *AuthorizationCodeGrant) Authorize(client *Client) (err error) {
err = client.SendRequest(
NewURI("%s/oauth/token", client.LoginURL),
&request.Options{
Authorization: request.BasicAuthorization(grant.ClientID, grant.Secret),
Authorization: request.BasicAuthorization(grant.ClientID.String(), grant.Secret),
Payload: map[string]string{
"grant_type": "authorization_code",
"code": grant.Code,
Expand Down
94 changes: 81 additions & 13 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,25 @@ package purecloud
import (
"fmt"
"net/url"
"strings"
"time"

"github.com/gildas/go-core"
"github.com/gildas/go-logger"
"github.com/google/uuid"
)

// Client is the primary object to use PureCloud
type Client struct {
Region string `json:"region"`
DeploymentID uuid.UUID `json:"deploymentId"`
Organization *Organization `json:"-"`
API *url.URL `json:"apiUrl,omitempty"`
LoginURL *url.URL `json:"loginUrl,omitempty"`
Proxy *url.URL `json:"proxyUrl,omitempty"`
AuthorizationGrant AuthorizationGrant `json:"auth"`
RequestTimeout time.Duration `json:"requestTimout"`
Logger *logger.Logger `json:"-"`
Region string `json:"region"`
DeploymentID uuid.UUID `json:"deploymentId"`
Organization *Organization `json:"-"`
API *url.URL `json:"apiUrl,omitempty"`
LoginURL *url.URL `json:"loginUrl,omitempty"`
Proxy *url.URL `json:"proxyUrl,omitempty"`
Grant Authorizer `json:"-"`
RequestTimeout time.Duration `json:"requestTimout"`
Logger *logger.Logger `json:"-"`
}

// ClientOptions contains the options to create a new Client
Expand All @@ -28,6 +30,7 @@ type ClientOptions struct {
OrganizationID uuid.UUID
DeploymentID uuid.UUID
Proxy *url.URL
Grant Authorizer
RequestTimeout time.Duration
Logger *logger.Logger
}
Expand All @@ -40,13 +43,14 @@ func NewClient(options *ClientOptions) *Client {
if len(options.Region) == 0 {
options.Region = "mypurecloud.com"
}
if options.RequestTimeout < 2 * time.Second {
if options.RequestTimeout < 2*time.Second {
options.RequestTimeout = 10 * time.Second
}
client := Client{
Proxy: options.Proxy,
DeploymentID: options.DeploymentID,
Organization: &Organization{ID: options.OrganizationID},
Grant: options.Grant,
RequestTimeout: options.RequestTimeout,
}
return client.SetLogger(options.Logger).SetRegion(options.Region)
Expand All @@ -67,18 +71,82 @@ func (client *Client) SetRegion(region string) *Client {
}

// SetAuthorizationGrant sets the Authorization Grant
func (client *Client) SetAuthorizationGrant(grant AuthorizationGrant) *Client {
client.AuthorizationGrant = grant
func (client *Client) SetAuthorizationGrant(grant Authorizer) *Client {
client.Grant = grant
return client
}

// IsAuthorized tells if the client has an Authorization Token
// It migt be expired and the app should login again as needed
func (client *Client) IsAuthorized() bool {
return client.AuthorizationGrant.AccessToken().IsValid()
return client.Grant.AccessToken().IsValid()
}

// Fetch fetches an initializable object
func (client *Client) Fetch(object Initializable) error {
return object.Initialize(client)
}

func (client *Client) CheckPermissions(permissions ...string) (permitted []string, missing []string) {
log := client.Logger.Child(nil, "checkpermissions")
subject, err := client.FetchRolesAndPermissions()
if err != nil {
return []string{}, permissions
}
permitted = []string{}
missing = []string{}
for _, desired := range permissions {
elements := strings.Split(desired, ":")
if len(elements) < 3 {
log.Warnf("This permission is invalid: %s (%d elements)", desired, len(elements))
missing = append(missing, desired)
break
}
desiredDomain := elements[0]
desiredEntity := elements[1]
desiredAction := elements[2]
found := false
log.Tracef("Checking Domain: %s, Entity: %s, Action: %s", desiredDomain, desiredEntity, desiredAction)
for _, grant := range subject.Grants {
for _, policy := range grant.Role.Policies {
if policy.Domain == desiredDomain && (policy.EntityName == "*" || desiredEntity == policy.EntityName) {
for _, action := range policy.Actions {
if action == "*" || action == desiredAction {
log.Tracef(" OK: %s:%s:%s", policy.Domain, policy.EntityName, action)
permitted = append(permitted, desired)
found = true
break
}
}
}
if found {
break
}
}
if found {
break
}
}
if !found {
missing = append(missing, desired)
}
}
return
}

// FetchRolesAndPermissions fetches roles and permissions for the current client
func (client *Client) FetchRolesAndPermissions() (*AuthorizationSubject, error) {
return client.FetchRolesAndPermissionsOf(client.Grant)
}

// FetchRolesAndPermissions fetches roles and permissions for the current client
func (client *Client) FetchRolesAndPermissionsOf(id core.Identifiable) (*AuthorizationSubject, error) {
log := client.Logger.Child(nil, "fetch_roles_permissions")
subject := AuthorizationSubject{}

log.Debugf("Fetching roles and permissions for %s", id.GetID())
if err := client.Get(NewURI("/authorization/subjects/%s", id.GetID().String()), &subject); err != nil {
return nil, err
}
return &subject, nil
}
6 changes: 3 additions & 3 deletions examples/OpenMessaging/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ func UpdateEnvFile(config *Config) {
config.Client.Logger.Infof("Updating the .env file")
_ = godotenv.Write(map[string]string{
"PURECLOUD_REGION": config.Client.Region,
"PURECLOUD_CLIENTID": config.Client.AuthorizationGrant.(*purecloud.ClientCredentialsGrant).ClientID.String(),
"PURECLOUD_CLIENTSECRET": config.Client.AuthorizationGrant.(*purecloud.ClientCredentialsGrant).Secret,
"PURECLOUD_CLIENTTOKEN": config.Client.AuthorizationGrant.AccessToken().Token,
"PURECLOUD_CLIENTID": config.Client.Grant.(*purecloud.ClientCredentialsGrant).ClientID.String(),
"PURECLOUD_CLIENTSECRET": config.Client.Grant.(*purecloud.ClientCredentialsGrant).Secret,
"PURECLOUD_CLIENTTOKEN": config.Client.Grant.AccessToken().Token,
"PURECLOUD_DEPLOYMENTID": config.Client.DeploymentID.String(),
"INTEGRATION_NAME": config.IntegrationName,
"INTEGRATION_WEBHOOK": config.IntegrationWebhookURL.String(),
Expand Down
2 changes: 1 addition & 1 deletion examples/auth_code/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ func main() {
DeploymentID: uuid.MustParse(*deploymentID),
Logger: Log,
}).SetAuthorizationGrant(&purecloud.AuthorizationCodeGrant{
ClientID: *clientID,
ClientID: uuid.MustParse(*clientID),
Secret: *secret,
RedirectURL: redirectURL,
})
Expand Down
2 changes: 1 addition & 1 deletion examples/chat_bot/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ func main() {
DeploymentID: uuid.MustParse(*deploymentID),
Logger: Log,
}).SetAuthorizationGrant(&purecloud.AuthorizationCodeGrant{
ClientID: *clientID,
ClientID: uuid.MustParse(*clientID),
Secret: *secret,
RedirectURL: redirectURL,
})
Expand Down
Loading

0 comments on commit b326061

Please sign in to comment.