package syncgroups import ( "fmt" "io" "net" "time" "github.com/golang/glog" "gopkg.in/ldap.v2" kapierrors "k8s.io/kubernetes/pkg/api/errors" "github.com/openshift/origin/pkg/auth/ldaputil" "github.com/openshift/origin/pkg/client" "github.com/openshift/origin/pkg/cmd/admin/groups/sync/interfaces" userapi "github.com/openshift/origin/pkg/user/api" ) // GroupSyncer runs a Sync job on Groups type GroupSyncer interface { // Sync syncs groups in OpenShift with records from an external source Sync() (groupsAffected []*userapi.Group, errors []error) } // LDAPGroupSyncer sync Groups with records on an external LDAP server type LDAPGroupSyncer struct { // Lists all groups to be synced GroupLister interfaces.LDAPGroupLister // Fetches a group and extracts object metainformation and membership list from a group GroupMemberExtractor interfaces.LDAPMemberExtractor // Maps an LDAP user entry to an OpenShift User's Name UserNameMapper interfaces.LDAPUserNameMapper // Maps an LDAP group enrty to an OpenShift Group's Name GroupNameMapper interfaces.LDAPGroupNameMapper // Allows the Syncer to search for OpenShift Groups GroupClient client.GroupInterface // Host stores the address:port of the LDAP server Host string // DryRun indicates that no changes should be made. DryRun bool // Out is used to provide output while the sync job is happening Out io.Writer Err io.Writer } var _ GroupSyncer = &LDAPGroupSyncer{} // Sync allows the LDAPGroupSyncer to be a GroupSyncer func (s *LDAPGroupSyncer) Sync() ([]*userapi.Group, []error) { openshiftGroups := []*userapi.Group{} var errors []error // determine what to sync glog.V(1).Infof("Listing with %v", s.GroupLister) ldapGroupUIDs, err := s.GroupLister.ListGroups() if err != nil { errors = append(errors, err) return nil, errors } glog.V(1).Infof("Sync ldapGroupUIDs %v", ldapGroupUIDs) for _, ldapGroupUID := range ldapGroupUIDs { glog.V(1).Infof("Checking LDAP group %v", ldapGroupUID) // get membership data memberEntries, err := s.GroupMemberExtractor.ExtractMembers(ldapGroupUID) if err != nil { fmt.Fprintf(s.Err, "Error determining LDAP group membership for %q: %v.\n", ldapGroupUID, err) errors = append(errors, err) continue } // determine OpenShift Users' usernames for LDAP group members usernames, err := s.determineUsernames(memberEntries) if err != nil { fmt.Fprintf(s.Err, "Error determining usernames for LDAP group %q: %v.\n", ldapGroupUID, err) errors = append(errors, err) continue } glog.V(1).Infof("Has OpenShift users %v", usernames) // update the OpenShift Group corresponding to this record openshiftGroup, err := s.makeOpenShiftGroup(ldapGroupUID, usernames) if err != nil { fmt.Fprintf(s.Err, "Error building OpenShift group for LDAP group %q: %v.\n", ldapGroupUID, err) errors = append(errors, err) continue } openshiftGroups = append(openshiftGroups, openshiftGroup) if !s.DryRun { fmt.Fprintf(s.Out, "group/%s\n", openshiftGroup.Name) if err := s.updateOpenShiftGroup(openshiftGroup); err != nil { fmt.Fprintf(s.Err, "Error updating OpenShift group %q for LDAP group %q: %v.\n", openshiftGroup.Name, ldapGroupUID, err) errors = append(errors, err) continue } } } return openshiftGroups, errors } // determineUsers determines the OpenShift Users that correspond to a list of LDAP member entries func (s *LDAPGroupSyncer) determineUsernames(members []*ldap.Entry) ([]string, error) { var usernames []string for _, member := range members { username, err := s.UserNameMapper.UserNameFor(member) if err != nil { return nil, err } glog.V(2).Infof("Found OpenShift username %q for LDAP user for %v", username, member) usernames = append(usernames, username) } return usernames, nil } // updateOpenShiftGroup creates the OpenShift Group in etcd func (s *LDAPGroupSyncer) updateOpenShiftGroup(openshiftGroup *userapi.Group) error { if len(openshiftGroup.UID) > 0 { _, err := s.GroupClient.Update(openshiftGroup) return err } _, err := s.GroupClient.Create(openshiftGroup) return err } // makeOpenShiftGroup creates the OpenShift Group object that needs to be updated, updates its data func (s *LDAPGroupSyncer) makeOpenShiftGroup(ldapGroupUID string, usernames []string) (*userapi.Group, error) { hostIP, _, err := net.SplitHostPort(s.Host) if err != nil { return nil, err } groupName, err := s.GroupNameMapper.GroupNameFor(ldapGroupUID) if err != nil { return nil, err } group, err := s.GroupClient.Get(groupName) if kapierrors.IsNotFound(err) { group = &userapi.Group{} group.Name = groupName group.Annotations = map[string]string{ ldaputil.LDAPURLAnnotation: s.Host, ldaputil.LDAPUIDAnnotation: ldapGroupUID, } group.Labels = map[string]string{ ldaputil.LDAPHostLabel: hostIP, } } else if err != nil { return nil, err } // make sure we aren't taking over an OpenShift group that is already related to a different LDAP group if host, exists := group.Labels[ldaputil.LDAPHostLabel]; !exists || (host != hostIP) { return nil, fmt.Errorf("group %q: %s label did not match sync host: wanted %s, got %s", group.Name, ldaputil.LDAPHostLabel, hostIP, host) } if url, exists := group.Annotations[ldaputil.LDAPURLAnnotation]; !exists || (url != s.Host) { return nil, fmt.Errorf("group %q: %s annotation did not match sync host: wanted %s, got %s", group.Name, ldaputil.LDAPURLAnnotation, s.Host, url) } if uid, exists := group.Annotations[ldaputil.LDAPUIDAnnotation]; !exists || (uid != ldapGroupUID) { return nil, fmt.Errorf("group %q: %s annotation did not match LDAP UID: wanted %s, got %s", group.Name, ldaputil.LDAPUIDAnnotation, ldapGroupUID, uid) } // overwrite Group Users data group.Users = usernames group.Annotations[ldaputil.LDAPSyncTimeAnnotation] = ISO8601(time.Now()) return group, nil } // ISO8601 returns an ISO 6801 formatted string from a time. func ISO8601(t time.Time) string { var tz string if zone, offset := t.Zone(); zone == "UTC" { tz = "Z" } else { tz = fmt.Sprintf("%03d00", offset/3600) } return fmt.Sprintf("%04d-%02d-%02dT%02d:%02d:%02d%s", t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), tz) }