Skip to content

Commit

Permalink
Merge pull request #87 from stlaz/groupsync_oidc
Browse files Browse the repository at this point in the history
AUTH-8: Add group synchronization from OIDC providers
  • Loading branch information
openshift-merge-robot authored Oct 18, 2021
2 parents 9d2b527 + 76a1b41 commit 6eb7ac3
Show file tree
Hide file tree
Showing 1,713 changed files with 140,684 additions and 55,333 deletions.
28 changes: 14 additions & 14 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,28 @@ require (
github.com/gorilla/context v0.0.0-20190627024605-8559d4a6b87e // indirect
github.com/gorilla/securecookie v0.0.0-20190707033817-86450627d8e6 // indirect
github.com/gorilla/sessions v0.0.0-20171008214740-a3acf13e802c
github.com/grpc-ecosystem/grpc-gateway v1.10.0 // indirect
github.com/openshift/api v0.0.0-20210331193751-3acddb19d360
github.com/openshift/build-machinery-go v0.0.0-20210423112049-9415d7ebd33e
github.com/openshift/client-go v0.0.0-20210331195552-cf6c2669e01f
github.com/openshift/library-go v0.0.0-20210414082648-6e767630a0dc
github.com/spf13/cobra v1.1.1
github.com/openshift/api v0.0.0-20211012185411-2e1b88be96db
github.com/openshift/build-machinery-go v0.0.0-20210806203541-4ea9b6da3a37
github.com/openshift/client-go v0.0.0-20210916133943-9acee1a0fb83
github.com/openshift/library-go v0.0.0-20211013122800-874db8a3dac9
github.com/spf13/cobra v1.1.3
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.7.0
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
golang.org/x/text v0.3.4
golang.org/x/text v0.3.6
gopkg.in/ldap.v2 v2.5.1
gopkg.in/square/go-jose.v2 v2.2.2
k8s.io/api v0.21.0
k8s.io/apimachinery v0.21.0
k8s.io/apiserver v0.21.0
k8s.io/client-go v0.21.0
k8s.io/component-base v0.21.0
k8s.io/klog/v2 v2.8.0
k8s.io/api v0.22.2
k8s.io/apimachinery v0.22.2
k8s.io/apiserver v0.22.2
k8s.io/client-go v0.22.2
k8s.io/component-base v0.22.2
k8s.io/klog/v2 v2.9.0
)

replace (
github.com/RangelReale/osin => github.com/openshift/osin v1.0.1-0.20180202150137-2dc1b4316769
github.com/RangelReale/osincli => github.com/openshift/osincli v0.0.0-20160924135400-fababb0555f2
k8s.io/apiserver => github.com/openshift/kubernetes-apiserver v0.0.0-20210416115049-61c04108f2c7 // points to openshift-apiserver-4.8-kubernetes-1.21.0
k8s.io/apiserver => github.com/openshift/kubernetes-apiserver v0.0.0-20210917123024-dee997b70b07 // points to openshift-apiserver-4.9-kubernetes-1.22.2
)
361 changes: 256 additions & 105 deletions go.sum

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions pkg/api/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,12 @@ type UserIdentityInfo interface {
GetProviderName() string
// GetProviderUserName uniquely identifies this particular identity for this provider. It is NOT guaranteed to be unique across providers
GetProviderUserName() string
// GetProviderGroups returns the names of the groups for this identity
GetProviderGroups() []string
// GetExtra is a map to allow providers to add additional fields that they understand
GetExtra() map[string]string
// GetProviderPreferredUserName is a shortcut to retrieve the preferred username from the Extra map
GetProviderPreferredUserName() string
}

// UserIdentityMapper maps UserIdentities into user.Info objects to allow different user abstractions within auth code.
Expand All @@ -59,6 +63,7 @@ type Grant struct {
type DefaultUserIdentityInfo struct {
ProviderName string
ProviderUserName string
ProviderGroups []string
Extra map[string]string
}

Expand All @@ -84,6 +89,17 @@ func (i *DefaultUserIdentityInfo) GetProviderUserName() string {
return i.ProviderUserName
}

func (i *DefaultUserIdentityInfo) GetProviderPreferredUserName() string {
if preferredUsername := i.Extra[IdentityPreferredUsernameKey]; len(preferredUsername) > 0 {
return preferredUsername
}
return i.ProviderUserName
}

func (i *DefaultUserIdentityInfo) GetProviderGroups() []string {
return i.ProviderGroups
}

func (i *DefaultUserIdentityInfo) GetExtra() map[string]string {
return i.Extra
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/authenticator/identitymapper/identitymapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import (
)

// ResponseFor bridges the UserIdentityMapper interface with the authenticator.{Password|Request} interfaces
func ResponseFor(mapper api.UserIdentityMapper, identity api.UserIdentityInfo) (*authenticator.Response, bool, error) {
user, err := mapper.UserFor(identity)
func ResponseFor(userMapper api.UserIdentityMapper, identity api.UserIdentityInfo) (*authenticator.Response, bool, error) {
user, err := userMapper.UserFor(identity)
if err != nil {
logf("error creating or updating mapping for: %#v due to %v", identity, err)
return nil, false, err
Expand Down
220 changes: 220 additions & 0 deletions pkg/groupmapper/groupmapper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
package groupmapper

import (
"context"
"fmt"
"time"

"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/wait"
kuser "k8s.io/apiserver/pkg/authentication/user"

userv1 "github.com/openshift/api/user/v1"
userclient "github.com/openshift/client-go/user/clientset/versioned/typed/user/v1"
userinformer "github.com/openshift/client-go/user/informers/externalversions/user/v1"
userlisterv1 "github.com/openshift/client-go/user/listers/user/v1"
usercache "github.com/openshift/library-go/pkg/oauth/usercache"

authapi "github.com/openshift/oauth-server/pkg/api"
)

const (
groupGeneratedKey = "oauth.openshift.io/generated"
groupSyncedKeyFmt = "oauth.openshift.io/idp.%s"
)

var _ authapi.UserIdentityMapper = &UserGroupsMapper{}

var _ kuser.Info = &UserInfoGroupsWrapper{}

// UserInfoGroupsWrapper wraps a UserInfo object in order to add extra groups
// retrieved from the identity providers
type UserInfoGroupsWrapper struct {
userInfo kuser.Info
additionalGroups sets.String
}

func (w *UserInfoGroupsWrapper) GetName() string {
return w.userInfo.GetName()
}

func (w *UserInfoGroupsWrapper) GetUID() string {
return w.userInfo.GetUID()
}

func (w *UserInfoGroupsWrapper) GetExtra() map[string][]string {
return w.userInfo.GetExtra()
}

func (w *UserInfoGroupsWrapper) GetGroups() []string {
groups := w.additionalGroups.Union(sets.NewString(w.userInfo.GetGroups()...))
return groups.List()
}

// UserGroupsMapper wraps a UserIdentityMapper with a struct that's capable to
// create the groups for a given user based on the provided UserIdentityInfo
type UserGroupsMapper struct {
delegatedUserMapper authapi.UserIdentityMapper
groupsClient userclient.GroupInterface
groupsLister userlisterv1.GroupLister
groupsCache *usercache.GroupCache
groupsSynced func() bool
}

func NewUserGroupsMapper(delegate authapi.UserIdentityMapper, groupInformer userinformer.GroupInformer, groupsClient userclient.GroupInterface, groupsLister userlisterv1.GroupLister) *UserGroupsMapper {
return &UserGroupsMapper{
delegatedUserMapper: delegate,
groupsClient: groupsClient,
groupsLister: groupsLister,
groupsCache: usercache.NewGroupCache(groupInformer),
groupsSynced: groupInformer.Informer().HasSynced,
}
}

func (m *UserGroupsMapper) UserFor(identityInfo authapi.UserIdentityInfo) (kuser.Info, error) {
userInfo, err := m.delegatedUserMapper.UserFor(identityInfo)
if err != nil {
return userInfo, err
}

identityGroups := sets.NewString(identityInfo.GetProviderGroups()...)
if err := m.processGroups(identityInfo.GetProviderName(), identityInfo.GetProviderPreferredUserName(), identityGroups); err != nil {
return nil, err
}

return &UserInfoGroupsWrapper{
userInfo: userInfo,
additionalGroups: identityGroups,
}, nil
}

func (m *UserGroupsMapper) processGroups(idpName, username string, groups sets.String) error {
err := wait.PollImmediate(1*time.Second, 5*time.Second, func() (bool, error) {
return m.groupsSynced(), nil
})
if err != nil {
return err
}

cachedGroups, err := m.groupsCache.GroupsFor(username)
if err != nil {
return err
}

removeGroups, addGroups := groupsDiff(cachedGroups, groups)
for _, g := range removeGroups {
if err := m.removeUserFromGroup(idpName, username, g); err != nil {
return err
}
}

for _, g := range addGroups {
if err := m.addUserToGroup(idpName, username, g); err != nil {
return err
}
}

return nil
}

func (m *UserGroupsMapper) removeUserFromGroup(idpName, username, group string) error {
updatedGroup, err := m.groupsLister.Get(group)
if err != nil {
if errors.IsNotFound(err) {
return nil
}
return err
}

if len(updatedGroup.Users) == 0 {
return nil
}

if len(updatedGroup.Users) == 1 && updatedGroup.Users[0] == username && updatedGroup.Annotations[groupGeneratedKey] == "true" {
return m.groupsClient.Delete(context.TODO(), group, metav1.DeleteOptions{})
}

// don't perform any actions on the group if it hasn't been synced for this IdP
if updatedGroup.Annotations[fmt.Sprintf(groupSyncedKeyFmt, idpName)] != "synced" {
return nil
}

// find the user and remove it from the slice
userIdx := -1
for i, groupUser := range updatedGroup.Users {
if groupUser == username {
userIdx = i
break
}
}

var newUsers []string
switch userIdx {
case -1:
return nil
case 0:
newUsers = updatedGroup.Users[1:]
default:
newUsers = append(updatedGroup.Users[0:userIdx], updatedGroup.Users[userIdx+1:]...)
}

updatedGroupCopy := updatedGroup.DeepCopy()
updatedGroupCopy.Users = newUsers

_, err = m.groupsClient.Update(context.TODO(), updatedGroupCopy, metav1.UpdateOptions{})
return err
}

func (m *UserGroupsMapper) addUserToGroup(idpName, username, group string) error {
updatedGroup, err := m.groupsLister.Get(group)
if errors.IsNotFound(err) {
_, err = m.groupsClient.Create(context.TODO(),
&userv1.Group{
ObjectMeta: metav1.ObjectMeta{
Name: group,
Annotations: map[string]string{
fmt.Sprintf(groupSyncedKeyFmt, idpName): "synced",
groupGeneratedKey: "true",
},
},
Users: []string{username},
},
metav1.CreateOptions{},
)
return err
}
if err != nil {
return err
}

var onlyAddAnnotation bool
for _, u := range updatedGroup.Users {
if u == username {
if updatedGroup.Annotations[fmt.Sprintf(groupSyncedKeyFmt, idpName)] != "synced" {
onlyAddAnnotation = true
break
}
return nil
}
}

updatedGroupCopy := updatedGroup.DeepCopy()
if !onlyAddAnnotation {
updatedGroupCopy.Users = append(updatedGroup.Users, username)
}
updatedGroupCopy.Annotations[fmt.Sprintf(groupSyncedKeyFmt, idpName)] = "synced"

_, err = m.groupsClient.Update(context.TODO(), updatedGroupCopy, metav1.UpdateOptions{})
return err
}

func groupsDiff(existing []*userv1.Group, required sets.String) (toRemove, toAdd []string) {
existingNames := sets.NewString()
for _, g := range existing {
existingNames.Insert(g.Name)
}

return existingNames.Difference(required).UnsortedList(), required.Difference(existingNames).UnsortedList()
}
Loading

0 comments on commit 6eb7ac3

Please sign in to comment.