Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,9 @@ services:
policy:
enabled: true
```

### Authorization

| Field | Description | Default |
| --- | --- | --- |
| `enabled` | Enable the Authorization
20 changes: 9 additions & 11 deletions opentdf-example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,15 @@ server:
policy:
## Default policy for all requests
default: #"role:readonly"
## Role map is used to map external roles to opentdf roles (opentdf_role:idp_role) the benefit of this is that you
## can use the builtin policy if desired
roles:
## Dot notation is used to access nested claims (i.e. realm_access.roles)
claim: # realm_access.roles
## Maps the external role to the opentdf role
## Note: left side is used in the policy, right side is the external role
map:
# readonly: opentdf-readonly
# admin: opentdf-admin
# org-admin: opentdf-org-admin
## Dot notation is used to access nested claims (i.e. realm_access.roles)
claim: # realm_access.roles
## Maps the external role to the opentdf role
## Note: left side is used in the policy, right side is the external role
map:
# readonly: opentdf-readonly
# admin: opentdf-admin
# org-admin: opentdf-org-admin

## Custom policy (see examples https://github.com/casbin/casbin/tree/master/examples)
csv: #|
# p, role:org-admin, policy:attributes, *, *, allow
Expand Down
54 changes: 35 additions & 19 deletions service/internal/auth/casbin.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import (
"fmt"
"strings"

"log/slog"

"github.com/casbin/casbin/v2"
casbinModel "github.com/casbin/casbin/v2/model"
stringadapter "github.com/casbin/casbin/v2/persist/string-adapter"
"github.com/lestrrat-go/jwx/v2/jwt"
"github.com/opentdf/platform/service/pkg/util"
"golang.org/x/exp/slog"
)

var rolePrefix = "role:"
Expand Down Expand Up @@ -151,6 +152,8 @@ func NewCasbinEnforcer(c CasbinConfig) (*Enforcer, error) {
pStr = c.Csv
}

slog.Debug("creating casbin enforcer", slog.Any("config", c))

m, err := casbinModel.NewModelFromString(mStr)
if err != nil {
return nil, fmt.Errorf("failed to create casbin model: %w", err)
Expand Down Expand Up @@ -208,6 +211,7 @@ func (e Enforcer) Enforce(token jwt.Token, resource, action string) (bool, error
}

func (e Enforcer) buildSubjectFromToken(t jwt.Token) (casbinSubject, error) {
slog.Debug("building subject from token", slog.Any("token", t))
roles, err := e.extractRolesFromToken(t)
if err != nil {
return casbinSubject{}, err
Expand All @@ -220,6 +224,7 @@ func (e Enforcer) buildSubjectFromToken(t jwt.Token) (casbinSubject, error) {
}

func (e Enforcer) extractRolesFromToken(t jwt.Token) ([]string, error) {
slog.Debug("extracting roles from token", slog.Any("token", t))
roles := []string{}

roleClaim := defaultRoleClaim
Expand All @@ -232,28 +237,39 @@ func (e Enforcer) extractRolesFromToken(t jwt.Token) ([]string, error) {
roleMap = e.Config.RoleMap
}

p := strings.Split(roleClaim, ".")
if n, ok := t.Get(p[0]); ok {
// use dotnotation if the claim is nested
r := n
if len(p) > 1 {
r = util.Dotnotation(n.(map[string]interface{}), strings.Join(p[1:], "."))
if r == nil {
slog.Warn("role claim not found", slog.String("claim", roleClaim), slog.Any("roles", n))
return nil, nil
}
selectors := strings.Split(roleClaim, ".")
claim, exists := t.Get(selectors[0])
if !exists {
slog.Warn("claim not found", slog.String("claim", roleClaim), slog.Any("token", t))
return nil, nil
}
slog.Debug("root claim found", slog.String("claim", roleClaim), slog.Any("claims", claim))
// use dotnotation if the claim is nested
if len(selectors) > 1 {
claimMap, ok := claim.(map[string]interface{})
if !ok {
slog.Warn("claim is not of type map[string]interface{}", slog.String("claim", roleClaim), slog.Any("claims", claim))
return nil, nil
}
claim = util.Dotnotation(claimMap, strings.Join(selectors[1:], "."))
if claim == nil {
slog.Warn("claim not found", slog.String("claim", roleClaim), slog.Any("claims", claim))
return nil, nil
}
}

// TODO test the type because an array of strings will panic
for _, v := range r.([]interface{}) {
switch vv := v.(type) {
case string:
roles = append(roles, vv)
default:
// check the type of the role claim
switch v := claim.(type) {
case string:
roles = append(roles, v)
case []interface{}:
for _, rr := range v {
if r, ok := rr.(string); ok {
roles = append(roles, r)
}
}
} else {
slog.Warn("role claim not found", slog.String("claim", roleClaim), slog.Any("token", t))
default:
slog.Warn("could not get claim type", slog.String("selector", roleClaim), slog.Any("claims", claim))
return nil, nil
}

Expand Down
98 changes: 67 additions & 31 deletions service/internal/auth/casbin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,26 +58,36 @@ func (s *AuthnCasbinSuite) buildTokenRoles(orgAdmin bool, admin bool, readonly b

func (s *AuthnCasbinSuite) newTokWithDefaultClaim(orgAdmin bool, admin bool, readonly bool) (string, jwt.Token) {
tok := jwt.New()
tok.Set("realm_access", map[string]interface{}{
"roles": s.buildTokenRoles(orgAdmin, admin, readonly, nil),
})
tokenRoles := s.buildTokenRoles(orgAdmin, admin, readonly, nil)
if err := tok.Set("realm_access", map[string]interface{}{"roles": tokenRoles}); err != nil {
s.T().Fatal(err)
}
return "", tok
}

func (s *AuthnCasbinSuite) newTokenWithCustomClaim(orgAdmin bool, admin bool, readonly bool) (string, jwt.Token) {
tok := jwt.New()
tok.Set("test", map[string]interface{}{
"test_roles": s.buildTokenRoles(orgAdmin, admin, readonly, nil),
})

return "test.test_roles", tok
tokenRoles := s.buildTokenRoles(orgAdmin, admin, readonly, nil)
if err := tok.Set("test", map[string]interface{}{"test_roles": map[string]interface{}{"roles": tokenRoles}}); err != nil {
s.T().Fatal(err)
}
return "test.test_roles.roles", tok
}

func (s *AuthnCasbinSuite) newTokenWithCustomRoleMap(orgAdmin bool, admin bool, readonly bool) (string, jwt.Token) {
tok := jwt.New()
tok.Set("realm_access", map[string]interface{}{
"roles": s.buildTokenRoles(orgAdmin, admin, readonly, []string{"test-org-admin", "test-admin", "test-readonly"}),
})
tokenRoles := s.buildTokenRoles(orgAdmin, admin, readonly, []string{"test-org-admin", "test-admin", "test-readonly"})
if err := tok.Set("realm_access", map[string]interface{}{"roles": tokenRoles}); err != nil {
s.T().Fatal(err)
}
return "", tok
}

func (s *AuthnCasbinSuite) newTokenWithCilentID() (string, jwt.Token) {
tok := jwt.New()
if err := tok.Set("client_id", "test"); err != nil {
s.T().Fatal(err)
}
return "", tok
}

Expand Down Expand Up @@ -118,15 +128,15 @@ func (s *AuthnCasbinSuite) Test_NewEnforcerWithCustomModel() {
Csv: "p, role:unknown, res, act, allow",
},
})
s.NoError(err)
s.Require().NoError(err)
s.NotNil(enforcer)

tok := jwt.New()
tok.Set("realm_access", map[string]interface{}{
"roles": []interface{}{"role:unknown"},
})
allowed, err := enforcer.Enforce(tok, "", "")
s.NoError(err)
s.Require().NoError(err)
s.True(allowed)
}

Expand Down Expand Up @@ -154,7 +164,7 @@ func (s *AuthnCasbinSuite) Test_Enforcement() {
resource string
action string
}{
// org-admin role
// // org-admin role
{
allowed: true,
roles: orgadmin,
Expand Down Expand Up @@ -285,46 +295,44 @@ func (s *AuthnCasbinSuite) Test_Enforcement() {

for _, test := range tests {
should := "should"
if !test.allowed {
should = "should not"
}
actor := ""
if test.roles[0] {
switch {
case test.roles[0]:
actor = "org-admin"
} else if test.roles[1] {
case test.roles[1]:
actor = "admin"
} else if test.roles[2] {
case test.roles[2]:
actor = "readonly"
} else {
default:
actor = "undefined"
}
name := fmt.Sprintf("%s **%s** be allowed to _%s_ %s resource", actor, should, test.action, test.resource)

slog.Info("running test w/ default claim", slog.String("name", name))
enforcer, err := NewCasbinEnforcer(CasbinConfig{})
s.NoError(err)
s.Require().NoError(err)
_, tok := s.newTokWithDefaultClaim(test.roles[0], test.roles[1], test.roles[2])
allowed, err := enforcer.Enforce(tok, test.resource, test.action)
if !test.allowed {
s.NotNil(err)
s.Require().Error(err)
} else {
s.NoError(err)
s.Require().NoError(err)
}
s.Equal(test.allowed, allowed)

slog.Info("running test w/ custom claim", slog.String("name", name))
enforcer, err = NewCasbinEnforcer(CasbinConfig{
PolicyConfig: PolicyConfig{
RoleClaim: "test.test_roles",
RoleClaim: "test.test_roles.roles",
},
})
s.NoError(err)
s.Require().NoError(err)
_, tok = s.newTokenWithCustomClaim(test.roles[0], test.roles[1], test.roles[2])
allowed, err = enforcer.Enforce(tok, test.resource, test.action)
if !test.allowed {
s.NotNil(err)
s.Require().Error(err)
} else {
s.NoError(err)
s.Require().NoError(err)
}
s.Equal(test.allowed, allowed)

Expand All @@ -338,14 +346,42 @@ func (s *AuthnCasbinSuite) Test_Enforcement() {
},
},
})
s.NoError(err)
s.Require().NoError(err)
_, tok = s.newTokenWithCustomRoleMap(test.roles[0], test.roles[1], test.roles[2])
allowed, err = enforcer.Enforce(tok, test.resource, test.action)
if !test.allowed {
s.NotNil(err)
s.Require().Error(err)
} else {
s.NoError(err)
s.Require().NoError(err)
}
s.Equal(test.allowed, allowed)
slog.Info("running test w/ client_id", slog.String("name", name))
var roleMap = make(map[string]string)
if test.roles[0] {
roleMap["org-admin"] = "test"
}
if test.roles[1] {
roleMap["admin"] = "test"
}
if test.roles[2] {
roleMap["readonly"] = "test"
}

enforcer, err = NewCasbinEnforcer(CasbinConfig{
PolicyConfig: PolicyConfig{
RoleClaim: "client_id",
RoleMap: roleMap,
},
})
s.Require().NoError(err)
_, tok = s.newTokenWithCilentID()
allowed, err = enforcer.Enforce(tok, test.resource, test.action)
if !test.allowed {
s.Require().Error(err)
} else {
s.Require().NoError(err)
}
s.Equal(test.allowed, allowed)
}

}
6 changes: 3 additions & 3 deletions service/internal/auth/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ type AuthNConfig struct {
Audience string `yaml:"audience" json:"audience"`
Clients []string `yaml:"clients" json:"clients"`
OIDCConfiguration `yaml:"-" json:"-"`
Policy PolicyConfig `yaml:"policy" json:"policy"`
Policy PolicyConfig `yaml:"policy" json:"policy" mapstructure:"policy"`
}

type PolicyConfig struct {
Default string `yaml:"default" json:"default"`
RoleClaim string `yaml:"claim" json:"claim"`
RoleMap map[string]string `yaml:"map" json:"map"`
RoleClaim string `yaml:"claim" json:"claim" mapstructure:"claim"`
RoleMap map[string]string `yaml:"map" json:"map" mapstructure:"map"`
Csv string `yaml:"csv" json:"csv"`
Model string `yaml:"model" json:"model"`
}
Expand Down