forked from TrueCloudLab/certificates
Merge pull request #711 from smallstep/oidc-admin-group
Check for admins in both emails and groups.
This commit is contained in:
commit
28bd2ef6c1
3 changed files with 69 additions and 39 deletions
|
@ -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
|
- go 1.17 to github action test matrix
|
||||||
- Support for CloudKMS RSA-PSS signers without using templates.
|
- Support for CloudKMS RSA-PSS signers without using templates.
|
||||||
- Add flags to support individual passwords for the intermediate and SSH keys.
|
- Add flags to support individual passwords for the intermediate and SSH keys.
|
||||||
|
- Global support for group admins in the OIDC provisioner.
|
||||||
### Changed
|
### Changed
|
||||||
- Using go 1.17 for binaries
|
- Using go 1.17 for binaries
|
||||||
### Deprecated
|
### Deprecated
|
||||||
|
|
|
@ -49,6 +49,29 @@ type openIDPayload struct {
|
||||||
Groups []string `json:"groups"`
|
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.
|
// OIDC represents an OAuth 2.0 OpenID Connect provider.
|
||||||
//
|
//
|
||||||
// ClientSecret is mandatory, but it can be an empty string.
|
// ClientSecret is mandatory, but it can be an empty string.
|
||||||
|
@ -73,35 +96,6 @@ type OIDC struct {
|
||||||
getIdentityFunc GetIdentityFunc
|
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 {
|
func sanitizeEmail(email string) string {
|
||||||
if i := strings.LastIndex(email, "@"); i >= 0 {
|
if i := strings.LastIndex(email, "@"); i >= 0 {
|
||||||
email = email[:i] + strings.ToLower(email[i:])
|
email = email[:i] + strings.ToLower(email[i:])
|
||||||
|
@ -234,7 +228,7 @@ func (o *OIDC) ValidatePayload(p openIDPayload) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate domains (case-insensitive)
|
// 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)
|
email := sanitizeEmail(p.Email)
|
||||||
var found bool
|
var found bool
|
||||||
for _, d := range o.Domains {
|
for _, d := range o.Domains {
|
||||||
|
@ -313,9 +307,10 @@ func (o *OIDC) AuthorizeRevoke(ctx context.Context, token string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only admins can revoke certificates.
|
// Only admins can revoke certificates.
|
||||||
if o.IsAdmin(claims.Email) {
|
if claims.IsAdmin(o.Admins) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return errs.Unauthorized("oidc.AuthorizeRevoke; cannot revoke with non-admin oidc token")
|
return errs.Unauthorized("oidc.AuthorizeRevoke; cannot revoke with non-admin oidc token")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
// Use the default template unless no-templates are configured and email is
|
||||||
// an admin, in that case we will use the CR template.
|
// an admin, in that case we will use the CR template.
|
||||||
defaultTemplate := x509util.DefaultLeafTemplate
|
defaultTemplate := x509util.DefaultLeafTemplate
|
||||||
if !o.Options.GetX509Options().HasTemplate() && o.IsAdmin(claims.Email) {
|
if !o.Options.GetX509Options().HasTemplate() && claims.IsAdmin(o.Admins) {
|
||||||
defaultTemplate = x509util.DefaultAdminLeafTemplate
|
defaultTemplate = x509util.DefaultAdminLeafTemplate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
// 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.
|
// an admin, in that case we will use the parameters in the request.
|
||||||
isAdmin := o.IsAdmin(claims.Email)
|
isAdmin := claims.IsAdmin(o.Admins)
|
||||||
if !isAdmin && len(claims.Groups) > 0 {
|
|
||||||
isAdmin = o.IsAdminGroup(claims.Groups)
|
|
||||||
}
|
|
||||||
defaultTemplate := sshutil.DefaultTemplate
|
defaultTemplate := sshutil.DefaultTemplate
|
||||||
if isAdmin && !o.Options.GetSSHOptions().HasTemplate() {
|
if isAdmin && !o.Options.GetSSHOptions().HasTemplate() {
|
||||||
defaultTemplate = sshutil.DefaultAdminTemplate
|
defaultTemplate = sshutil.DefaultAdminTemplate
|
||||||
|
@ -471,10 +463,11 @@ func (o *OIDC) AuthorizeSSHRevoke(ctx context.Context, token string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only admins can revoke certificates.
|
// Only admins can revoke certificates.
|
||||||
if !o.IsAdmin(claims.Email) {
|
if claims.IsAdmin(o.Admins) {
|
||||||
return errs.Unauthorized("oidc.AuthorizeSSHRevoke; cannot revoke with non-admin oidc token")
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return errs.Unauthorized("oidc.AuthorizeSSHRevoke; cannot revoke with non-admin oidc token")
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAndDecode(uri string, v interface{}) error {
|
func getAndDecode(uri string, v interface{}) error {
|
||||||
|
|
|
@ -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{"admin@smallstep.com", nil}, args{[]string{"admin@smallstep.com"}}, true},
|
||||||
|
{"ok email multiple", fields{"admin@smallstep.com", []string{"admin", "eng"}}, args{[]string{"eng@smallstep.com", "admin@smallstep.com"}}, true},
|
||||||
|
{"ok email sanitized", fields{"admin@Smallstep.com", nil}, args{[]string{"admin@smallStep.com"}}, true},
|
||||||
|
{"ok group", fields{"", []string{"admin"}}, args{[]string{"admin"}}, true},
|
||||||
|
{"ok group multiple", fields{"admin@smallstep.com", []string{"engineering", "admin"}}, args{[]string{"admin"}}, true},
|
||||||
|
{"fail missing", fields{"eng@smallstep.com", []string{"admin"}}, args{[]string{"admin@smallstep.com"}}, false},
|
||||||
|
{"fail email letter case", fields{"Admin@smallstep.com", []string{}}, args{[]string{"admin@smallstep.com"}}, 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue