Skip to content

Commit

Permalink
Merge pull request #711 from smallstep/oidc-admin-group
Browse files Browse the repository at this point in the history
Check for admins in both emails and groups.
  • Loading branch information
maraino authored Sep 24, 2021
2 parents 7f00cc7 + 963eaf8 commit 28bd2ef
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 39 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- go 1.17 to github action test matrix
- Support for CloudKMS RSA-PSS signers without using templates.
- Add flags to support individual passwords for the intermediate and SSH keys.
- Global support for group admins in the OIDC provisioner.
### Changed
- Using go 1.17 for binaries
### Deprecated
Expand Down
71 changes: 32 additions & 39 deletions authority/provisioner/oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,29 @@ type openIDPayload struct {
Groups []string `json:"groups"`
}

func (o *openIDPayload) IsAdmin(admins []string) bool {
if o.Email != "" {
email := sanitizeEmail(o.Email)
for _, e := range admins {
if email == sanitizeEmail(e) {
return true
}
}
}

// The groups and emails can be in the same array for now, but consider
// making a specialized option later.
for _, name := range o.Groups {
for _, admin := range admins {
if name == admin {
return true
}
}
}

return false
}

// OIDC represents an OAuth 2.0 OpenID Connect provider.
//
// ClientSecret is mandatory, but it can be an empty string.
Expand All @@ -73,35 +96,6 @@ type OIDC struct {
getIdentityFunc GetIdentityFunc
}

// IsAdmin returns true if the given email is in the Admins allowlist, false
// otherwise.
func (o *OIDC) IsAdmin(email string) bool {
if email != "" {
email = sanitizeEmail(email)
for _, e := range o.Admins {
if email == sanitizeEmail(e) {
return true
}
}
}
return false
}

// IsAdminGroup returns true if the one group in the given list is in the Admins
// allowlist, false otherwise.
func (o *OIDC) IsAdminGroup(groups []string) bool {
for _, g := range groups {
// The groups and emails can be in the same array for now, but consider
// making a specialized option later.
for _, gadmin := range o.Admins {
if g == gadmin {
return true
}
}
}
return false
}

func sanitizeEmail(email string) string {
if i := strings.LastIndex(email, "@"); i >= 0 {
email = email[:i] + strings.ToLower(email[i:])
Expand Down Expand Up @@ -234,7 +228,7 @@ func (o *OIDC) ValidatePayload(p openIDPayload) error {
}

// Validate domains (case-insensitive)
if p.Email != "" && len(o.Domains) > 0 && !o.IsAdmin(p.Email) {
if p.Email != "" && len(o.Domains) > 0 && !p.IsAdmin(o.Admins) {
email := sanitizeEmail(p.Email)
var found bool
for _, d := range o.Domains {
Expand Down Expand Up @@ -313,9 +307,10 @@ func (o *OIDC) AuthorizeRevoke(ctx context.Context, token string) error {
}

// Only admins can revoke certificates.
if o.IsAdmin(claims.Email) {
if claims.IsAdmin(o.Admins) {
return nil
}

return errs.Unauthorized("oidc.AuthorizeRevoke; cannot revoke with non-admin oidc token")
}

Expand Down Expand Up @@ -351,7 +346,7 @@ func (o *OIDC) AuthorizeSign(ctx context.Context, token string) ([]SignOption, e
// Use the default template unless no-templates are configured and email is
// an admin, in that case we will use the CR template.
defaultTemplate := x509util.DefaultLeafTemplate
if !o.Options.GetX509Options().HasTemplate() && o.IsAdmin(claims.Email) {
if !o.Options.GetX509Options().HasTemplate() && claims.IsAdmin(o.Admins) {
defaultTemplate = x509util.DefaultAdminLeafTemplate
}

Expand Down Expand Up @@ -420,10 +415,7 @@ func (o *OIDC) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption

// Use the default template unless no-templates are configured and email is
// an admin, in that case we will use the parameters in the request.
isAdmin := o.IsAdmin(claims.Email)
if !isAdmin && len(claims.Groups) > 0 {
isAdmin = o.IsAdminGroup(claims.Groups)
}
isAdmin := claims.IsAdmin(o.Admins)
defaultTemplate := sshutil.DefaultTemplate
if isAdmin && !o.Options.GetSSHOptions().HasTemplate() {
defaultTemplate = sshutil.DefaultAdminTemplate
Expand Down Expand Up @@ -471,10 +463,11 @@ func (o *OIDC) AuthorizeSSHRevoke(ctx context.Context, token string) error {
}

// Only admins can revoke certificates.
if !o.IsAdmin(claims.Email) {
return errs.Unauthorized("oidc.AuthorizeSSHRevoke; cannot revoke with non-admin oidc token")
if claims.IsAdmin(o.Admins) {
return nil
}
return nil

return errs.Unauthorized("oidc.AuthorizeSSHRevoke; cannot revoke with non-admin oidc token")
}

func getAndDecode(uri string, v interface{}) error {
Expand Down
36 changes: 36 additions & 0 deletions authority/provisioner/oidc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -698,3 +698,39 @@ func Test_sanitizeEmail(t *testing.T) {
})
}
}

func Test_openIDPayload_IsAdmin(t *testing.T) {
type fields struct {
Email string
Groups []string
}
type args struct {
admins []string
}
tests := []struct {
name string
fields fields
args args
want bool
}{
{"ok email", fields{"[email protected]", nil}, args{[]string{"[email protected]"}}, true},
{"ok email multiple", fields{"[email protected]", []string{"admin", "eng"}}, args{[]string{"[email protected]", "[email protected]"}}, true},
{"ok email sanitized", fields{"[email protected]", nil}, args{[]string{"[email protected]"}}, true},
{"ok group", fields{"", []string{"admin"}}, args{[]string{"admin"}}, true},
{"ok group multiple", fields{"[email protected]", []string{"engineering", "admin"}}, args{[]string{"admin"}}, true},
{"fail missing", fields{"[email protected]", []string{"admin"}}, args{[]string{"[email protected]"}}, false},
{"fail email letter case", fields{"[email protected]", []string{}}, args{[]string{"[email protected]"}}, false},
{"fail group letter case", fields{"", []string{"Admin"}}, args{[]string{"admin"}}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
o := &openIDPayload{
Email: tt.fields.Email,
Groups: tt.fields.Groups,
}
if got := o.IsAdmin(tt.args.admins); got != tt.want {
t.Errorf("openIDPayload.IsAdmin() = %v, want %v", got, tt.want)
}
})
}
}

0 comments on commit 28bd2ef

Please sign in to comment.