Skip to content

Commit

Permalink
Implement OAuth Authorizer (#4306)
Browse files Browse the repository at this point in the history
  • Loading branch information
iamrodrigo authored Jul 20, 2021
1 parent 7db7654 commit 9f5d412
Show file tree
Hide file tree
Showing 23 changed files with 800 additions and 17 deletions.
2 changes: 1 addition & 1 deletion cmd/server/cadence/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ func (s *server) startService() common.Daemon {
params.ArchiverProvider = provider.NewArchiverProvider(s.cfg.Archival.History.Provider, s.cfg.Archival.Visibility.Provider)
params.PersistenceConfig.TransactionSizeLimit = dc.GetIntProperty(dynamicconfig.TransactionSizeLimit, common.DefaultTransactionSizeLimit)
params.PersistenceConfig.ErrorInjectionRate = dc.GetFloat64Property(dynamicconfig.PersistenceErrorInjectionRate, 0)
params.Authorizer = authorization.NewNopAuthorizer()
params.Authorizer = authorization.NewAuthorizer(s.cfg.Authorization, params.Logger)
params.BlobstoreClient, err = filestore.NewFilestoreClient(s.cfg.Blobstore.Filestore)
if err != nil {
log.Printf("failed to create file blobstore client, will continue startup without it: %v", err)
Expand Down
26 changes: 26 additions & 0 deletions common/authorization/authorizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ const (
DecisionAllow
)

const (
// PermissionRead means the user can write on the domain level APIs
PermissionRead Permission = iota + 1
// PermissionWrite means the user can write on the domain level APIs
PermissionWrite
// PermissionAdmin means the user can read+write on the domain level APIs
PermissionAdmin
)

type (
// Attributes is input for authority to make decision.
// It can be extended in future if required auth on resources like WorkflowType and TaskList
Expand All @@ -43,6 +52,7 @@ type (
APIName string
DomainName string
TaskList *types.TaskList
Permission Permission
}

// Result is result from authority.
Expand All @@ -52,8 +62,24 @@ type (

// Decision is enum type for auth decision
Decision int

// Permission is enum type for auth permission
Permission int
)

func NewPermission(permission string) Permission {
switch permission {
case "read":
return PermissionRead
case "write":
return PermissionWrite
case "admin":
return PermissionAdmin
default:
return -1
}
}

// Authorizer is an interface for authorization
type Authorizer interface {
Authorize(ctx context.Context, attributes *Attributes) (Result, error)
Expand Down
35 changes: 35 additions & 0 deletions common/authorization/factory.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright (c) 2021 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

package authorization

import (
"github.com/uber/cadence/common/config"
"github.com/uber/cadence/common/log"
)

func NewAuthorizer(authorization config.Authorization, logger log.Logger) Authorizer {
switch true {
case authorization.OAuthAuthorizer.Enable:
return NewOAuthAuthorizer(authorization.OAuthAuthorizer, logger)
default:
return NewNopAuthorizer()
}
}
88 changes: 88 additions & 0 deletions common/authorization/factory_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Copyright (c) 2021 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

package authorization

import (
"testing"

"github.com/cristalhq/jwt/v3"
"github.com/stretchr/testify/suite"

"github.com/uber/cadence/common/config"
"github.com/uber/cadence/common/log"
"github.com/uber/cadence/common/log/loggerimpl"
)

type (
factorySuite struct {
suite.Suite
logger log.Logger
}
)

func TestFactorySuite(t *testing.T) {
suite.Run(t, new(factorySuite))
}

func (s *factorySuite) SetupTest() {
s.logger = loggerimpl.NewLoggerForTest(s.Suite)
}

func cfgNoop() config.Authorization {
return config.Authorization{
OAuthAuthorizer: config.OAuthAuthorizer{
Enable: false,
},
NoopAuthorizer: config.NoopAuthorizer{
Enable: true,
},
}
}

func cfgOAuth() config.Authorization {
return config.Authorization{
OAuthAuthorizer: config.OAuthAuthorizer{
Enable: true,
JwtCredentials: config.JwtCredentials{
Algorithm: jwt.RS256.String(),
PublicKey: "public",
PrivateKey: "private",
},
MaxJwtTTL: 12345,
},
}
}

func (s *factorySuite) TestFactoryNoopAuthorizer() {
cfgOAuthVar := cfgOAuth()
var tests = []struct {
cfg config.Authorization
expected Authorizer
}{
{cfgNoop(), &nopAuthority{}},
{cfgOAuthVar, &oauthAuthority{authorizationCfg: cfgOAuthVar.OAuthAuthorizer, log: s.logger}},
}

for _, test := range tests {
authorizer := NewAuthorizer(test.cfg, s.logger)
s.Equal(authorizer, test.expected)
}
}
125 changes: 125 additions & 0 deletions common/authorization/oauthAuthorizer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// Copyright (c) 2021 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

package authorization

import (
"context"
"encoding/json"
"fmt"
"time"

"github.com/cristalhq/jwt/v3"
"go.uber.org/yarpc"

"github.com/uber/cadence/common"
"github.com/uber/cadence/common/config"
"github.com/uber/cadence/common/log"
"github.com/uber/cadence/common/log/tag"
)

type oauthAuthority struct {
authorizationCfg config.OAuthAuthorizer
log log.Logger
}

type jwtClaims struct {
Sub string
Name string
Permission string
Domain string
Iat int64
TTL int64
}

// NewOAuthAuthorizer creates a oauth authority
func NewOAuthAuthorizer(
authorizationCfg config.OAuthAuthorizer,
log log.Logger,
) Authorizer {
return &oauthAuthority{
authorizationCfg: authorizationCfg,
log: log,
}
}

// Authorize defines the logic to verify get claims from token
func (a *oauthAuthority) Authorize(
ctx context.Context,
attributes *Attributes,
) (Result, error) {
call := yarpc.CallFromContext(ctx)
verifier, err := a.getVerifier()
if err != nil {
return Result{Decision: DecisionDeny}, err
}
token := call.Header(common.AuthorizationTokenHeaderName)
claims, err := a.parseToken(token, verifier)
if err != nil {
a.log.Debug("request is not authorized", tag.Error(err))
return Result{Decision: DecisionDeny}, nil
}
err = a.validateClaims(claims, attributes)
if err != nil {
a.log.Debug("request is not authorized", tag.Error(err))
return Result{Decision: DecisionDeny}, nil
}
return Result{Decision: DecisionAllow}, nil
}

func (a *oauthAuthority) getVerifier() (jwt.Verifier, error) {
publicKey, err := common.StringToRSAPublicKey(a.authorizationCfg.JwtCredentials.PublicKey)
if err != nil {
return nil, err
}
algorithm := jwt.Algorithm(a.authorizationCfg.JwtCredentials.Algorithm)
verifier, err := jwt.NewVerifierRS(algorithm, publicKey)
if err != nil {
return nil, err
}
return verifier, nil
}

func (a *oauthAuthority) parseToken(tokenStr string, verifier jwt.Verifier) (*jwtClaims, error) {
token, verifyErr := jwt.ParseAndVerifyString(tokenStr, verifier)
if verifyErr != nil {
return nil, verifyErr
}
var claims jwtClaims
_ = json.Unmarshal(token.RawClaims(), &claims)
return &claims, nil
}

func (a *oauthAuthority) validateClaims(claims *jwtClaims, attributes *Attributes) error {
if claims.TTL > a.authorizationCfg.MaxJwtTTL {
return fmt.Errorf("TTL in token is larger than MaxTTL allowed")
}
if claims.Iat+claims.TTL < time.Now().Unix() {
return fmt.Errorf("JWT has expired")
}
if claims.Domain != attributes.DomainName {
return fmt.Errorf("domain in token doesn't match with current domain")
}
if NewPermission(claims.Permission) < attributes.Permission {
return fmt.Errorf("token doesn't have the right permission")
}

return nil
}
Loading

0 comments on commit 9f5d412

Please sign in to comment.