| ... | ... |
@@ -39,7 +39,7 @@ OpenShift `Group`'s `Labels` will also be used to store metadata regarding the s |
| 39 | 39 |
* Populate new OpenShift `Group`'s `Users` from resulting OpenShift `UserIdentityMappings`, update OpenShift `Group` metadata fields tied to LDAP attributes, leave other OpenShift `Group` fields unchanged |
| 40 | 40 |
|
| 41 | 41 |
## Determining Ordered List of LDAP Groups To Sync |
| 42 |
-An `LDAPGroupLister` determines what LDAP groups needs to be synced and outputs the result as a set of unique identifier strings (called "LDAP group UIDs" in this document). The other objects that take LDAP group UIDs need to understand the format of this string (e.g. these objects will be tightly coupled). For example, an `LDAPGroupLister` for schema 1 above cannot be used with a `LDAPGroupDataExtractor` for schema 2. The four `LDAPGroupLister` implementations that are necessary are: |
|
| 42 |
+An `LDAPGroupLister` determines what LDAP groups needs to be synced and outputs the result as a set of unique identifier strings (called "LDAP group UIDs" in this document). The other objects that take LDAP group UIDs need to understand the format of this string (e.g. these objects will be tightly coupled). For example, an `LDAPGroupLister` for schema 1 above cannot be used with a `LDAPGroupMemberExtractor` for schema 2. The four `LDAPGroupLister` implementations that are necessary are: |
|
| 43 | 43 |
|
| 44 | 44 |
1. List LDAP group UIDs for all OpenShift `Groups` matching a `Label` selector identifying them as pertaining to the sync job, minus a blacklist |
| 45 | 45 |
* List LDAP group UIDs for some whitelist of OpenShift `Groups` |
| ... | ... |
@@ -48,7 +48,7 @@ An `LDAPGroupLister` determines what LDAP groups needs to be synced and outputs |
| 48 | 48 |
|
| 49 | 49 |
```go |
| 50 | 50 |
// LDAPGroupLister lists the LDAP groups that need to be synced by a job. The LDAPGroupLister needs to |
| 51 |
-// be paired with an LDAPGroupDataExtractor that understands the format of the unique identifiers |
|
| 51 |
+// be paired with an LDAPGroupMemberExtractor that understands the format of the unique identifiers |
|
| 52 | 52 |
// returned to represent the LDAP groups to be synced. |
| 53 | 53 |
type LDAPGroupLister interface {
|
| 54 | 54 |
ListGroups() (groupUIDs []string, err error) |
| ... | ... |
@@ -56,11 +56,11 @@ type LDAPGroupLister interface {
|
| 56 | 56 |
``` |
| 57 | 57 |
|
| 58 | 58 |
## Collecting LDAP Members and Metadata |
| 59 |
-An `LDAPGroupDataExtractor` gathers information on an LDAP group based on an LDAP group UID. It may cache LDAP responses for responsivity. The approach to implementing this structure will vary with LDAP schema as well as sync job request. |
|
| 59 |
+An `LDAPGroupMemberExtractor` gathers information on an LDAP group based on an LDAP group UID. It may cache LDAP responses for responsivity. The approach to implementing this structure will vary with LDAP schema as well as sync job request. |
|
| 60 | 60 |
|
| 61 | 61 |
```go |
| 62 |
-// LDAPGroupDataExtractor retrieves data about an LDAP group from the LDAP server. |
|
| 63 |
-type LDAPGroupDataExtractor interface {
|
|
| 62 |
+// LDAPGroupMemberExtractor retrieves data about an LDAP group from the LDAP server. |
|
| 63 |
+type LDAPGroupMemberExtractor interface {
|
|
| 64 | 64 |
// ExtractMembers returns the list of LDAP first-class user entries that are members of the LDAP |
| 65 | 65 |
// group specified by the groupUID |
| 66 | 66 |
ExtractMembers(groupUID string) (members []*ldap.Entry, err error) |
| ... | ... |
@@ -68,7 +68,7 @@ type LDAPGroupDataExtractor interface {
|
| 68 | 68 |
``` |
| 69 | 69 |
|
| 70 | 70 |
## Determining OpenShift `User` Names for LDAP Members |
| 71 |
-The mapping of a LDAP member entry to an OpenShift `User` Name will be deterministic and simple: whatever LDAP entry attribute is used for the OpenShift `User` Name field upon creation of OpenShift `Users` will be used as the OpenShift `User` Name. As long as the `DeterministicUserIdentityMapper` is used to introduce LDAP member entries to OpenShift `User` records and the `LDAPUserToIdentityMapping` used for the sync job and `DeterministicUserIdentityMapper` is the same, the mappings created by the `LDAPUserNameMapper` will be correct. |
|
| 71 |
+The mapping of a LDAP member entry to an OpenShift `User` Name will be deterministic and simple: whatever LDAP entry attribute is used for the OpenShift `User` Name field upon creation of OpenShift `Users` will be used as the OpenShift `User` Name. As long as the `DeterministicUserIdentityMapper` is used to introduce LDAP member entries to OpenShift `User` records and the `LDAPUserAttributeDefiner` used for the sync job and `DeterministicUserIdentityMapper` is the same, the mappings created by the `LDAPUserNameMapper` will be correct. |
|
| 72 | 72 |
|
| 73 | 73 |
```go |
| 74 | 74 |
// LDAPUserNameMapper maps an LDAP entry representing a user to the OpenShift User Name corresponding to it |
| ... | ... |
@@ -22,11 +22,6 @@ type Options struct {
|
| 22 | 22 |
// ClientConfig holds information about connecting with the LDAP server |
| 23 | 23 |
ClientConfig ldaputil.LDAPClientConfig |
| 24 | 24 |
|
| 25 |
- // BindDN is the optional username to bind to for the search phase. If specified, BindPassword must also be set. |
|
| 26 |
- BindDN string |
|
| 27 |
- // BindPassword is the optional password to bind to for the search phase. |
|
| 28 |
- BindPassword string |
|
| 29 |
- |
|
| 30 | 25 |
// UserAttributeDefiner defines the values corresponding to OpenShift Identities in LDAP entries |
| 31 | 26 |
// by using a deterministic mapping of LDAP entry attributes to OpenShift Identity fields. The first |
| 32 | 27 |
// attribute with a non-empty value is used for all but the latter identity field. If no LDAP attributes |
| ... | ... |
@@ -88,18 +83,15 @@ func (a *Authenticator) getIdentity(username, password string) (authapi.UserIden |
| 88 | 88 |
return nil, false, nil |
| 89 | 89 |
} |
| 90 | 90 |
|
| 91 |
- // Make the connection |
|
| 91 |
+ // Make the connection and bind to it if a bind DN and password were given |
|
| 92 | 92 |
l, err := a.options.ClientConfig.Connect() |
| 93 | 93 |
if err != nil {
|
| 94 | 94 |
return nil, false, err |
| 95 | 95 |
} |
| 96 | 96 |
defer l.Close() |
| 97 | 97 |
|
| 98 |
- // If specified, bind the username/password for search phase |
|
| 99 |
- if len(a.options.BindDN) > 0 {
|
|
| 100 |
- if err := l.Bind(a.options.BindDN, a.options.BindPassword); err != nil {
|
|
| 101 |
- return nil, false, err |
|
| 102 |
- } |
|
| 98 |
+ if _, err := a.options.ClientConfig.Bind(l); err != nil {
|
|
| 99 |
+ return nil, false, err |
|
| 103 | 100 |
} |
| 104 | 101 |
|
| 105 | 102 |
// & together the filter specified in the LDAP options with the user-specific filter |
| ... | ... |
@@ -5,25 +5,47 @@ import ( |
| 5 | 5 |
"fmt" |
| 6 | 6 |
"net" |
| 7 | 7 |
|
| 8 |
+ "k8s.io/kubernetes/pkg/util" |
|
| 9 |
+ |
|
| 8 | 10 |
"github.com/go-ldap/ldap" |
| 9 | 11 |
) |
| 10 | 12 |
|
| 11 | 13 |
// NewLDAPClientConfig returns a new LDAPClientConfig |
| 12 |
-func NewLDAPClientConfig(url LDAPURL, insecure bool, tlsConfig *tls.Config) LDAPClientConfig {
|
|
| 13 |
- return LDAPClientConfig{
|
|
| 14 |
- Scheme: url.Scheme, |
|
| 15 |
- Host: url.Host, |
|
| 16 |
- Insecure: insecure, |
|
| 17 |
- TLSConfig: tlsConfig, |
|
| 14 |
+func NewLDAPClientConfig(URL, bindDN, bindPassword, CA string, insecure bool) (LDAPClientConfig, error) {
|
|
| 15 |
+ url, err := ParseURL(URL) |
|
| 16 |
+ if err != nil {
|
|
| 17 |
+ return LDAPClientConfig{}, fmt.Errorf("Error parsing URL: %v", err)
|
|
| 18 | 18 |
} |
| 19 |
+ |
|
| 20 |
+ tlsConfig := &tls.Config{}
|
|
| 21 |
+ if len(CA) > 0 {
|
|
| 22 |
+ roots, err := util.CertPoolFromFile(CA) |
|
| 23 |
+ if err != nil {
|
|
| 24 |
+ return LDAPClientConfig{}, fmt.Errorf("error loading cert pool from ca file %s: %v", CA, err)
|
|
| 25 |
+ } |
|
| 26 |
+ tlsConfig.RootCAs = roots |
|
| 27 |
+ } |
|
| 28 |
+ |
|
| 29 |
+ return LDAPClientConfig{
|
|
| 30 |
+ Scheme: url.Scheme, |
|
| 31 |
+ Host: url.Host, |
|
| 32 |
+ BindDN: bindDN, |
|
| 33 |
+ BindPassword: bindPassword, |
|
| 34 |
+ Insecure: insecure, |
|
| 35 |
+ TLSConfig: tlsConfig, |
|
| 36 |
+ }, nil |
|
| 19 | 37 |
} |
| 20 | 38 |
|
| 21 | 39 |
// LDAPClientConfig holds information for connecting to an LDAP server |
| 22 | 40 |
type LDAPClientConfig struct {
|
| 23 |
- // Scheme is ldap or ldaps |
|
| 41 |
+ // Scheme is the LDAP connection scheme, either ldap or ldaps |
|
| 24 | 42 |
Scheme Scheme |
| 25 | 43 |
// Host is the host:port of the LDAP server |
| 26 | 44 |
Host string |
| 45 |
+ // BindDN is an optional DN to bind with during the search phase. |
|
| 46 |
+ BindDN string |
|
| 47 |
+ // BindPassword is an optional password to bind with during the search phase. |
|
| 48 |
+ BindPassword string |
|
| 27 | 49 |
// Insecure specifies if TLS is required for the connection. If true, either an ldap://... URL or |
| 28 | 50 |
// StartTLS must be supported by the server |
| 29 | 51 |
Insecure bool |
| ... | ... |
@@ -31,9 +53,9 @@ type LDAPClientConfig struct {
|
| 31 | 31 |
TLSConfig *tls.Config |
| 32 | 32 |
} |
| 33 | 33 |
|
| 34 |
-// Connect returns an established LDAP connection, or an error if the connection could not be made |
|
| 35 |
-// (or successfully upgraded to TLS). If no error is returned, the caller is responsible for closing |
|
| 36 |
-// the connection |
|
| 34 |
+// Connect returns an established LDAP connection, or an error if the connection could not |
|
| 35 |
+// be made (or successfully upgraded to TLS). If no error is returned, the caller is responsible for |
|
| 36 |
+// closing the connection |
|
| 37 | 37 |
func (l *LDAPClientConfig) Connect() (*ldap.Conn, error) {
|
| 38 | 38 |
tlsConfig := l.TLSConfig |
| 39 | 39 |
|
| ... | ... |
@@ -78,3 +100,17 @@ func (l *LDAPClientConfig) Connect() (*ldap.Conn, error) {
|
| 78 | 78 |
return nil, fmt.Errorf("unsupported scheme %q", l.Scheme)
|
| 79 | 79 |
} |
| 80 | 80 |
} |
| 81 |
+ |
|
| 82 |
+// Bind binds to a given LDAP connection if a bind DN and password were given. |
|
| 83 |
+// Bind returns whether a bind occured and whether an error occurred |
|
| 84 |
+func (l *LDAPClientConfig) Bind(connection *ldap.Conn) (bound bool, err error) {
|
|
| 85 |
+ if len(l.BindDN) > 0 {
|
|
| 86 |
+ if err := connection.Bind(l.BindDN, l.BindPassword); err != nil {
|
|
| 87 |
+ return false, err |
|
| 88 |
+ } else {
|
|
| 89 |
+ return true, nil |
|
| 90 |
+ } |
|
| 91 |
+ } |
|
| 92 |
+ |
|
| 93 |
+ return false, nil |
|
| 94 |
+} |
| 81 | 95 |
deleted file mode 100644 |
| ... | ... |
@@ -1,71 +0,0 @@ |
| 1 |
-package ldaputil |
|
| 2 |
- |
|
| 3 |
-import ( |
|
| 4 |
- "testing" |
|
| 5 |
- |
|
| 6 |
- "github.com/go-ldap/ldap" |
|
| 7 |
-) |
|
| 8 |
- |
|
| 9 |
-func TestGetAttributeValue(t *testing.T) {
|
|
| 10 |
- |
|
| 11 |
- testcases := map[string]struct {
|
|
| 12 |
- Entry *ldap.Entry |
|
| 13 |
- Attributes []string |
|
| 14 |
- ExpectedValue string |
|
| 15 |
- }{
|
|
| 16 |
- "empty": {
|
|
| 17 |
- Attributes: []string{},
|
|
| 18 |
- Entry: &ldap.Entry{DN: "", Attributes: []*ldap.EntryAttribute{}},
|
|
| 19 |
- ExpectedValue: "", |
|
| 20 |
- }, |
|
| 21 |
- |
|
| 22 |
- "dn": {
|
|
| 23 |
- Attributes: []string{"dn"},
|
|
| 24 |
- Entry: &ldap.Entry{DN: "foo", Attributes: []*ldap.EntryAttribute{}},
|
|
| 25 |
- ExpectedValue: "foo", |
|
| 26 |
- }, |
|
| 27 |
- "DN": {
|
|
| 28 |
- Attributes: []string{"DN"},
|
|
| 29 |
- Entry: &ldap.Entry{DN: "foo", Attributes: []*ldap.EntryAttribute{}},
|
|
| 30 |
- ExpectedValue: "foo", |
|
| 31 |
- }, |
|
| 32 |
- |
|
| 33 |
- "missing": {
|
|
| 34 |
- Attributes: []string{"foo", "bar", "baz"},
|
|
| 35 |
- Entry: &ldap.Entry{DN: "", Attributes: []*ldap.EntryAttribute{}},
|
|
| 36 |
- ExpectedValue: "", |
|
| 37 |
- }, |
|
| 38 |
- |
|
| 39 |
- "present": {
|
|
| 40 |
- Attributes: []string{"foo"},
|
|
| 41 |
- Entry: &ldap.Entry{DN: "", Attributes: []*ldap.EntryAttribute{
|
|
| 42 |
- {Name: "foo", Values: []string{"fooValue"}},
|
|
| 43 |
- }}, |
|
| 44 |
- ExpectedValue: "fooValue", |
|
| 45 |
- }, |
|
| 46 |
- "first of multi-value attribute": {
|
|
| 47 |
- Attributes: []string{"foo"},
|
|
| 48 |
- Entry: &ldap.Entry{DN: "", Attributes: []*ldap.EntryAttribute{
|
|
| 49 |
- {Name: "foo", Values: []string{"fooValue", "fooValue2"}},
|
|
| 50 |
- }}, |
|
| 51 |
- ExpectedValue: "fooValue", |
|
| 52 |
- }, |
|
| 53 |
- "first present attribute": {
|
|
| 54 |
- Attributes: []string{"foo", "bar", "baz"},
|
|
| 55 |
- Entry: &ldap.Entry{DN: "", Attributes: []*ldap.EntryAttribute{
|
|
| 56 |
- {Name: "foo", Values: []string{""}},
|
|
| 57 |
- {Name: "bar", Values: []string{"barValue"}},
|
|
| 58 |
- {Name: "baz", Values: []string{"bazValue"}},
|
|
| 59 |
- }}, |
|
| 60 |
- ExpectedValue: "barValue", |
|
| 61 |
- }, |
|
| 62 |
- } |
|
| 63 |
- |
|
| 64 |
- for k, tc := range testcases {
|
|
| 65 |
- v := getAttributeValue(tc.Entry, tc.Attributes) |
|
| 66 |
- if v != tc.ExpectedValue {
|
|
| 67 |
- t.Errorf("%s: Expected %q, got %q", k, tc.ExpectedValue, v)
|
|
| 68 |
- } |
|
| 69 |
- } |
|
| 70 |
- |
|
| 71 |
-} |
| ... | ... |
@@ -72,28 +72,28 @@ func (d *LDAPUserAttributeDefiner) AllAttributes() util.StringSet {
|
| 72 | 72 |
|
| 73 | 73 |
// Email extracts the email value from an LDAP user entry |
| 74 | 74 |
func (d *LDAPUserAttributeDefiner) Email(user *ldap.Entry) string {
|
| 75 |
- return getAttributeValue(user, d.attributeMapping.Email) |
|
| 75 |
+ return GetAttributeValue(user, d.attributeMapping.Email) |
|
| 76 | 76 |
} |
| 77 | 77 |
|
| 78 | 78 |
// Name extracts the name value from an LDAP user entry |
| 79 | 79 |
func (d *LDAPUserAttributeDefiner) Name(user *ldap.Entry) string {
|
| 80 |
- return getAttributeValue(user, d.attributeMapping.Name) |
|
| 80 |
+ return GetAttributeValue(user, d.attributeMapping.Name) |
|
| 81 | 81 |
} |
| 82 | 82 |
|
| 83 | 83 |
// PreferredUsername extracts the preferred username value from an LDAP user entry |
| 84 | 84 |
func (d *LDAPUserAttributeDefiner) PreferredUsername(user *ldap.Entry) string {
|
| 85 |
- return getAttributeValue(user, d.attributeMapping.PreferredUsername) |
|
| 85 |
+ return GetAttributeValue(user, d.attributeMapping.PreferredUsername) |
|
| 86 | 86 |
} |
| 87 | 87 |
|
| 88 | 88 |
// ID extracts the ID value from an LDAP user entry |
| 89 | 89 |
func (d *LDAPUserAttributeDefiner) ID(user *ldap.Entry) string {
|
| 90 |
- return getAttributeValue(user, d.attributeMapping.ID) |
|
| 90 |
+ return GetAttributeValue(user, d.attributeMapping.ID) |
|
| 91 | 91 |
} |
| 92 | 92 |
|
| 93 |
-// getAttributeValue finds the first attribute of those given that the LDAP entry has, and |
|
| 94 |
-// returns it. getAttributeValue is able to query the DN as well as Attributes of the LDAP entry. |
|
| 93 |
+// GetAttributeValue finds the first attribute of those given that the LDAP entry has, and |
|
| 94 |
+// returns it. GetAttributeValue is able to query the DN as well as Attributes of the LDAP entry. |
|
| 95 | 95 |
// If no value is found, the empty string is returned. |
| 96 |
-func getAttributeValue(entry *ldap.Entry, attributes []string) string {
|
|
| 96 |
+func GetAttributeValue(entry *ldap.Entry, attributes []string) string {
|
|
| 97 | 97 |
for _, k := range attributes {
|
| 98 | 98 |
// Ignore empty attributes |
| 99 | 99 |
if len(k) == 0 {
|
| 100 | 100 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,71 @@ |
| 0 |
+package ldaputil |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "testing" |
|
| 4 |
+ |
|
| 5 |
+ "github.com/go-ldap/ldap" |
|
| 6 |
+) |
|
| 7 |
+ |
|
| 8 |
+func TestGetAttributeValue(t *testing.T) {
|
|
| 9 |
+ |
|
| 10 |
+ testcases := map[string]struct {
|
|
| 11 |
+ Entry *ldap.Entry |
|
| 12 |
+ Attributes []string |
|
| 13 |
+ ExpectedValue string |
|
| 14 |
+ }{
|
|
| 15 |
+ "empty": {
|
|
| 16 |
+ Attributes: []string{},
|
|
| 17 |
+ Entry: &ldap.Entry{DN: "", Attributes: []*ldap.EntryAttribute{}},
|
|
| 18 |
+ ExpectedValue: "", |
|
| 19 |
+ }, |
|
| 20 |
+ |
|
| 21 |
+ "dn": {
|
|
| 22 |
+ Attributes: []string{"dn"},
|
|
| 23 |
+ Entry: &ldap.Entry{DN: "foo", Attributes: []*ldap.EntryAttribute{}},
|
|
| 24 |
+ ExpectedValue: "foo", |
|
| 25 |
+ }, |
|
| 26 |
+ "DN": {
|
|
| 27 |
+ Attributes: []string{"DN"},
|
|
| 28 |
+ Entry: &ldap.Entry{DN: "foo", Attributes: []*ldap.EntryAttribute{}},
|
|
| 29 |
+ ExpectedValue: "foo", |
|
| 30 |
+ }, |
|
| 31 |
+ |
|
| 32 |
+ "missing": {
|
|
| 33 |
+ Attributes: []string{"foo", "bar", "baz"},
|
|
| 34 |
+ Entry: &ldap.Entry{DN: "", Attributes: []*ldap.EntryAttribute{}},
|
|
| 35 |
+ ExpectedValue: "", |
|
| 36 |
+ }, |
|
| 37 |
+ |
|
| 38 |
+ "present": {
|
|
| 39 |
+ Attributes: []string{"foo"},
|
|
| 40 |
+ Entry: &ldap.Entry{DN: "", Attributes: []*ldap.EntryAttribute{
|
|
| 41 |
+ {Name: "foo", Values: []string{"fooValue"}},
|
|
| 42 |
+ }}, |
|
| 43 |
+ ExpectedValue: "fooValue", |
|
| 44 |
+ }, |
|
| 45 |
+ "first of multi-value attribute": {
|
|
| 46 |
+ Attributes: []string{"foo"},
|
|
| 47 |
+ Entry: &ldap.Entry{DN: "", Attributes: []*ldap.EntryAttribute{
|
|
| 48 |
+ {Name: "foo", Values: []string{"fooValue", "fooValue2"}},
|
|
| 49 |
+ }}, |
|
| 50 |
+ ExpectedValue: "fooValue", |
|
| 51 |
+ }, |
|
| 52 |
+ "first present attribute": {
|
|
| 53 |
+ Attributes: []string{"foo", "bar", "baz"},
|
|
| 54 |
+ Entry: &ldap.Entry{DN: "", Attributes: []*ldap.EntryAttribute{
|
|
| 55 |
+ {Name: "foo", Values: []string{""}},
|
|
| 56 |
+ {Name: "bar", Values: []string{"barValue"}},
|
|
| 57 |
+ {Name: "baz", Values: []string{"bazValue"}},
|
|
| 58 |
+ }}, |
|
| 59 |
+ ExpectedValue: "barValue", |
|
| 60 |
+ }, |
|
| 61 |
+ } |
|
| 62 |
+ |
|
| 63 |
+ for k, tc := range testcases {
|
|
| 64 |
+ v := GetAttributeValue(tc.Entry, tc.Attributes) |
|
| 65 |
+ if v != tc.ExpectedValue {
|
|
| 66 |
+ t.Errorf("%s: Expected %q, got %q", k, tc.ExpectedValue, v)
|
|
| 67 |
+ } |
|
| 68 |
+ } |
|
| 69 |
+ |
|
| 70 |
+} |
| 0 | 71 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,211 @@ |
| 0 |
+package ldaputil |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "fmt" |
|
| 4 |
+ "strings" |
|
| 5 |
+ |
|
| 6 |
+ "github.com/go-ldap/ldap" |
|
| 7 |
+ "github.com/golang/glog" |
|
| 8 |
+ |
|
| 9 |
+ "k8s.io/kubernetes/pkg/util" |
|
| 10 |
+ |
|
| 11 |
+ "github.com/openshift/origin/pkg/cmd/server/api" |
|
| 12 |
+) |
|
| 13 |
+ |
|
| 14 |
+// LDAPQuery encodes an LDAP query |
|
| 15 |
+type LDAPQuery struct {
|
|
| 16 |
+ // The DN of the branch of the directory where all searches should start from |
|
| 17 |
+ BaseDN string |
|
| 18 |
+ |
|
| 19 |
+ // The (optional) scope of the search. Defaults to the entire subtree if not set |
|
| 20 |
+ Scope Scope |
|
| 21 |
+ |
|
| 22 |
+ // The (optional) behavior of the search with regards to alisases. Defaults to always |
|
| 23 |
+ // dereferencing if not set |
|
| 24 |
+ DerefAliases DerefAliases |
|
| 25 |
+ |
|
| 26 |
+ // TimeLimit holds the limit of time in seconds that any request to the server can remain outstanding |
|
| 27 |
+ // before the wait for a response is given up. If this is 0, no client-side limit is imposed |
|
| 28 |
+ TimeLimit int |
|
| 29 |
+ |
|
| 30 |
+ // Filter is a valid LDAP search filter that retrieves all relevant entries from the LDAP server with the base DN |
|
| 31 |
+ Filter string |
|
| 32 |
+} |
|
| 33 |
+ |
|
| 34 |
+// NewSearchRequest creates a new search request for the LDAP query and optionally includes more attributes |
|
| 35 |
+func (q *LDAPQuery) NewSearchRequest(additionalAttributes []string) *ldap.SearchRequest {
|
|
| 36 |
+ return ldap.NewSearchRequest( |
|
| 37 |
+ q.BaseDN, |
|
| 38 |
+ int(q.Scope), |
|
| 39 |
+ int(q.DerefAliases), |
|
| 40 |
+ 0, // allowed return size - indicates no limit |
|
| 41 |
+ q.TimeLimit, |
|
| 42 |
+ false, // not types only |
|
| 43 |
+ q.Filter, |
|
| 44 |
+ additionalAttributes, |
|
| 45 |
+ nil, // no controls |
|
| 46 |
+ ) |
|
| 47 |
+} |
|
| 48 |
+ |
|
| 49 |
+// LDAPQueryOnAttribute encodes an LDAP query that conjoins two filters to extract a specific LDAP entry |
|
| 50 |
+// This query is not self-sufficient and needs the value of the QueryAttribute to construct the final filter |
|
| 51 |
+type LDAPQueryOnAttribute struct {
|
|
| 52 |
+ // Query retrieves entries from an LDAP server |
|
| 53 |
+ LDAPQuery |
|
| 54 |
+ |
|
| 55 |
+ // QueryAttribute is the attribute for a specific filter that, when conjoined with the common filter, |
|
| 56 |
+ // retrieves the specific LDAP entry from the LDAP server. (e.g. "cn", when formatted with "aGroupName" |
|
| 57 |
+ // and conjoined with "objectClass=groupOfNames", becomes (&(objectClass=groupOfNames)(cn=aGroupName))") |
|
| 58 |
+ QueryAttribute string |
|
| 59 |
+} |
|
| 60 |
+ |
|
| 61 |
+// IdentifiyingLDAPQueryOptions holds a query and the attribute that identifies the entries that the query returns |
|
| 62 |
+type IdentifiyingLDAPQueryOptions struct {
|
|
| 63 |
+ // Query retrieves entries from an LDAP server |
|
| 64 |
+ LDAPQueryOnAttribute |
|
| 65 |
+ |
|
| 66 |
+ // NameAttributes defines the attributes for the LDAP entries returned by the Query that will be interpreted |
|
| 67 |
+ // as their names |
|
| 68 |
+ NameAttributes []string |
|
| 69 |
+} |
|
| 70 |
+ |
|
| 71 |
+// NewIdentifiyingLDAPQueryOptions converts a user-provided LDAPQuery into a version we can use by parsing |
|
| 72 |
+// the input and combining it with a set of name attributes |
|
| 73 |
+func NewIdentifiyingLDAPQueryOptions(config api.LDAPQuery, |
|
| 74 |
+ nameAttributes []string) (IdentifiyingLDAPQueryOptions, error) {
|
|
| 75 |
+ |
|
| 76 |
+ scope, err := DetermineLDAPScope(config.Scope) |
|
| 77 |
+ if err != nil {
|
|
| 78 |
+ return IdentifiyingLDAPQueryOptions{}, err
|
|
| 79 |
+ } |
|
| 80 |
+ |
|
| 81 |
+ derefAliases, err := DetermineDerefAliasesBehavior(config.DerefAliases) |
|
| 82 |
+ if err != nil {
|
|
| 83 |
+ return IdentifiyingLDAPQueryOptions{}, err
|
|
| 84 |
+ } |
|
| 85 |
+ |
|
| 86 |
+ return IdentifiyingLDAPQueryOptions{
|
|
| 87 |
+ LDAPQueryOnAttribute: LDAPQueryOnAttribute{
|
|
| 88 |
+ LDAPQuery: LDAPQuery{
|
|
| 89 |
+ BaseDN: config.BaseDN, |
|
| 90 |
+ Scope: scope, |
|
| 91 |
+ DerefAliases: derefAliases, |
|
| 92 |
+ TimeLimit: config.TimeLimit, |
|
| 93 |
+ Filter: config.Filter, |
|
| 94 |
+ }, |
|
| 95 |
+ QueryAttribute: config.QueryAttribute, |
|
| 96 |
+ }, |
|
| 97 |
+ NameAttributes: nameAttributes, |
|
| 98 |
+ }, nil |
|
| 99 |
+} |
|
| 100 |
+ |
|
| 101 |
+// NewSearchRequest creates a new search request from the identifying query by internalizing the value of |
|
| 102 |
+// the attribute to be filtered as well as any additional attributest that need to be recovereds |
|
| 103 |
+func (o *IdentifiyingLDAPQueryOptions) NewSearchRequest(attributeValue string, |
|
| 104 |
+ additionalAttributes []string) (*ldap.SearchRequest, error) {
|
|
| 105 |
+ |
|
| 106 |
+ allAttributes := util.NewStringSet(o.NameAttributes...) |
|
| 107 |
+ allAttributes.Insert(additionalAttributes...) |
|
| 108 |
+ |
|
| 109 |
+ if o.QueryAttribute == "DN" || o.QueryAttribute == "dn" {
|
|
| 110 |
+ if !strings.Contains(attributeValue, o.BaseDN) {
|
|
| 111 |
+ return nil, fmt.Errorf("search for entry with %s=%s would search outside of the base dn specified (dn=%s)",
|
|
| 112 |
+ o.QueryAttribute, attributeValue, o.BaseDN) |
|
| 113 |
+ } |
|
| 114 |
+ if _, err := ldap.ParseDN(attributeValue); err != nil {
|
|
| 115 |
+ return nil, fmt.Errorf("could not search by dn, invalid dn value: %v", err)
|
|
| 116 |
+ } |
|
| 117 |
+ return o.buildDNQuery(attributeValue, allAttributes.List()), nil |
|
| 118 |
+ } else {
|
|
| 119 |
+ return o.buildAttributeQuery(attributeValue, allAttributes.List()), nil |
|
| 120 |
+ } |
|
| 121 |
+} |
|
| 122 |
+ |
|
| 123 |
+// buildDNQuery builds the query that finds an LDAP entry with the given DN |
|
| 124 |
+// this is done by setting the DN to be the base DN for the search and setting the search scope |
|
| 125 |
+// to only consider the base object found |
|
| 126 |
+func (o *IdentifiyingLDAPQueryOptions) buildDNQuery(dn string, |
|
| 127 |
+ attributes []string) *ldap.SearchRequest {
|
|
| 128 |
+ return ldap.NewSearchRequest( |
|
| 129 |
+ dn, |
|
| 130 |
+ ldap.ScopeBaseObject, // over-ride original |
|
| 131 |
+ int(o.DerefAliases), |
|
| 132 |
+ 0, // allowed return size - indicates no limit |
|
| 133 |
+ o.TimeLimit, |
|
| 134 |
+ false, // not types only |
|
| 135 |
+ "objectClass=*", // filter that returns all values |
|
| 136 |
+ attributes, |
|
| 137 |
+ nil, // no controls |
|
| 138 |
+ ) |
|
| 139 |
+} |
|
| 140 |
+ |
|
| 141 |
+// buildAttributeQuery builds the query containing a filter that conjoins the common filter given |
|
| 142 |
+// in the configuration with the specific attribute filter for which the attribute value is given |
|
| 143 |
+func (o *IdentifiyingLDAPQueryOptions) buildAttributeQuery(attributeValue string, |
|
| 144 |
+ attributes []string) *ldap.SearchRequest {
|
|
| 145 |
+ specificFilter := fmt.Sprintf("%s=%s",
|
|
| 146 |
+ ldap.EscapeFilter(o.QueryAttribute), |
|
| 147 |
+ ldap.EscapeFilter(attributeValue)) |
|
| 148 |
+ |
|
| 149 |
+ filter := fmt.Sprintf("(&(%s)(%s))", o.Filter, specificFilter)
|
|
| 150 |
+ |
|
| 151 |
+ return ldap.NewSearchRequest( |
|
| 152 |
+ o.BaseDN, |
|
| 153 |
+ int(o.Scope), |
|
| 154 |
+ int(o.DerefAliases), |
|
| 155 |
+ 0, // allowed return size - indicates no limit |
|
| 156 |
+ o.TimeLimit, |
|
| 157 |
+ false, // not types only |
|
| 158 |
+ filter, |
|
| 159 |
+ attributes, |
|
| 160 |
+ nil, // no controls |
|
| 161 |
+ ) |
|
| 162 |
+} |
|
| 163 |
+ |
|
| 164 |
+// QueryForUniqueEntry queries for an LDAP entry on an LDAP server determined from a ClientConfig |
|
| 165 |
+// by creating a search request from the requisite input. The query is expected to return one unqiue |
|
| 166 |
+// result. If this is not the case, errors are raised |
|
| 167 |
+func QueryForUniqueEntry(clientConfig LDAPClientConfig, |
|
| 168 |
+ query *ldap.SearchRequest) (entry *ldap.Entry, err error) {
|
|
| 169 |
+ |
|
| 170 |
+ result, err := QueryForEntries(clientConfig, query) |
|
| 171 |
+ if err != nil {
|
|
| 172 |
+ return nil, err |
|
| 173 |
+ } |
|
| 174 |
+ if len(result) > 1 {
|
|
| 175 |
+ return nil, fmt.Errorf("multiple entries found matching %s", query.Filter)
|
|
| 176 |
+ } |
|
| 177 |
+ entry = result[0] |
|
| 178 |
+ glog.V(4).Infof("found dn=%q for %s", entry.DN, query.Filter)
|
|
| 179 |
+ return entry, nil |
|
| 180 |
+} |
|
| 181 |
+ |
|
| 182 |
+// QueryForEntries queries for LDAP entries on an LDAP server determined from a ClientConfig by |
|
| 183 |
+// creating a search request from the requisite input. |
|
| 184 |
+func QueryForEntries(clientConfig LDAPClientConfig, |
|
| 185 |
+ query *ldap.SearchRequest) (result []*ldap.Entry, err error) {
|
|
| 186 |
+ |
|
| 187 |
+ connection, err := clientConfig.Connect() |
|
| 188 |
+ if err != nil {
|
|
| 189 |
+ return nil, err |
|
| 190 |
+ } |
|
| 191 |
+ defer connection.Close() |
|
| 192 |
+ |
|
| 193 |
+ glog.V(4).Infof("searching LDAP server for %s", query.Filter)
|
|
| 194 |
+ searchResult, err := connection.Search(query) |
|
| 195 |
+ if err != nil {
|
|
| 196 |
+ return nil, err |
|
| 197 |
+ } |
|
| 198 |
+ |
|
| 199 |
+ entries := searchResult.Entries |
|
| 200 |
+ // No entries returned from the LDAP search request means that the LDAP search was not configured |
|
| 201 |
+ // correctly. The search must return with at least one LDAP entry |
|
| 202 |
+ if len(entries) == 0 {
|
|
| 203 |
+ return nil, fmt.Errorf("no LDAP user entry found for filter: %s", query.Filter)
|
|
| 204 |
+ } |
|
| 205 |
+ |
|
| 206 |
+ for _, entry := range entries {
|
|
| 207 |
+ glog.V(4).Infof("found dn=%q for %s", entry.DN, query.Filter)
|
|
| 208 |
+ } |
|
| 209 |
+ return entries, nil |
|
| 210 |
+} |
| 0 | 211 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,11 @@ |
| 0 |
+package ldaputil |
|
| 1 |
+ |
|
| 2 |
+// These constants contain values for annotations and labels affixed to Groups by the LDAP sync job |
|
| 3 |
+const ( |
|
| 4 |
+ // LDAPURLAnnotation is the Annotation value that stores the host:port of the LDAP server |
|
| 5 |
+ LDAPURLAnnotation string = "openshift.io/ldap.url" |
|
| 6 |
+ // LDAPUIDAnnotation is the Annotation value that stores the corresponding LDAP group UID for the Group |
|
| 7 |
+ LDAPUIDAnnotation string = "openshift.io/ldap.uid" |
|
| 8 |
+ // LDAPSyncTime is the Annotation value that stores the last time this Group was synced with LDAP |
|
| 9 |
+ LDAPSyncTimeAnnotation string = "openshift.io/ldap.sync-time" |
|
| 10 |
+) |
| ... | ... |
@@ -242,7 +242,7 @@ func DetermineDerefAliasesBehavior(derefAliasesString string) (DerefAliases, err |
| 242 | 242 |
} |
| 243 | 243 |
derefAliases, exists := mapping[derefAliasesString] |
| 244 | 244 |
if !exists {
|
| 245 |
- return -1, fmt.Errorf("not a valid LDAP search scope: %s", derefAliasesString)
|
|
| 245 |
+ return -1, fmt.Errorf("not a valid LDAP alias dereferncing behavior: %s", derefAliasesString)
|
|
| 246 | 246 |
} |
| 247 | 247 |
return derefAliases, nil |
| 248 | 248 |
} |
| 249 | 249 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,124 @@ |
| 0 |
+package syncgroups |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "fmt" |
|
| 4 |
+ |
|
| 5 |
+ "k8s.io/kubernetes/pkg/fields" |
|
| 6 |
+ "k8s.io/kubernetes/pkg/labels" |
|
| 7 |
+ |
|
| 8 |
+ "github.com/openshift/origin/pkg/auth/ldaputil" |
|
| 9 |
+ osclient "github.com/openshift/origin/pkg/client" |
|
| 10 |
+ "github.com/openshift/origin/pkg/cmd/experimental/syncgroups/interfaces" |
|
| 11 |
+ ouserapi "github.com/openshift/origin/pkg/user/api" |
|
| 12 |
+) |
|
| 13 |
+ |
|
| 14 |
+// NewAllOpenShiftGroupLister returns a new AllLocalGroupLister |
|
| 15 |
+func NewAllOpenShiftGroupLister(ldapURL string, groupClient osclient.GroupInterface) interfaces.LDAPGroupLister {
|
|
| 16 |
+ return &AllLocalGroupLister{
|
|
| 17 |
+ client: groupClient, |
|
| 18 |
+ ldapURL: ldapURL, |
|
| 19 |
+ } |
|
| 20 |
+} |
|
| 21 |
+ |
|
| 22 |
+// AllLocalGroupLister lists unique identifiers for LDAP lookup of all local OpenShift Groups that |
|
| 23 |
+// have been marked with an LDAP URL annotation as a result of a previous sync. |
|
| 24 |
+type AllLocalGroupLister struct {
|
|
| 25 |
+ client osclient.GroupInterface |
|
| 26 |
+ // ldapURL is the host:port of the LDAP server, used to identify if an OpenShift Group has |
|
| 27 |
+ // been synced with a specific server in order to isolate sync jobs between different servers |
|
| 28 |
+ ldapURL string |
|
| 29 |
+} |
|
| 30 |
+ |
|
| 31 |
+func (l *AllLocalGroupLister) ListGroups() (ldapGroupUIDs []string, err error) {
|
|
| 32 |
+ allGroups, err := l.client.List(labels.Everything(), fields.Everything()) |
|
| 33 |
+ if err != nil {
|
|
| 34 |
+ return nil, err |
|
| 35 |
+ } |
|
| 36 |
+ |
|
| 37 |
+ var potentialGroups []ouserapi.Group |
|
| 38 |
+ for _, group := range allGroups.Items {
|
|
| 39 |
+ val, exists := group.Annotations[ldaputil.LDAPURLAnnotation] |
|
| 40 |
+ if exists && (val == l.ldapURL) {
|
|
| 41 |
+ potentialGroups = append(potentialGroups, group) |
|
| 42 |
+ } |
|
| 43 |
+ } |
|
| 44 |
+ |
|
| 45 |
+ for _, group := range potentialGroups {
|
|
| 46 |
+ if err := validateGroupAnnotations(group); err != nil {
|
|
| 47 |
+ return nil, err |
|
| 48 |
+ } |
|
| 49 |
+ ldapGroupUIDs = append(ldapGroupUIDs, group.Annotations[ldaputil.LDAPUIDAnnotation]) |
|
| 50 |
+ } |
|
| 51 |
+ return ldapGroupUIDs, nil |
|
| 52 |
+} |
|
| 53 |
+ |
|
| 54 |
+// validateGroupAnnotations determines if the appropriate and annotations exist on a group |
|
| 55 |
+func validateGroupAnnotations(group ouserapi.Group) error {
|
|
| 56 |
+ _, exists := group.Annotations[ldaputil.LDAPUIDAnnotation] |
|
| 57 |
+ if !exists {
|
|
| 58 |
+ return fmt.Errorf("an OpenShift Group marked as having been synced did not have a %s annotation: %v",
|
|
| 59 |
+ ldaputil.LDAPUIDAnnotation, group) |
|
| 60 |
+ } |
|
| 61 |
+ return nil |
|
| 62 |
+} |
|
| 63 |
+ |
|
| 64 |
+// NewOpenShiftWhitelistGroupLister returns a new LocalGroupLister that divulges the LDAP group unique identifier for |
|
| 65 |
+// each entry in the given whitelist of OpenShift Group names |
|
| 66 |
+func NewOpenShiftWhitelistGroupLister(whitelist []string, client osclient.GroupInterface) interfaces.LDAPGroupLister {
|
|
| 67 |
+ return &LocalGroupLister{
|
|
| 68 |
+ whitelist: whitelist, |
|
| 69 |
+ client: client, |
|
| 70 |
+ } |
|
| 71 |
+} |
|
| 72 |
+ |
|
| 73 |
+// LocalGroupLister lists unique identifiers for LDAP lookup of all local OpenShift groups that have |
|
| 74 |
+// been given to it upon creation. |
|
| 75 |
+type LocalGroupLister struct {
|
|
| 76 |
+ whitelist []string |
|
| 77 |
+ client osclient.GroupInterface |
|
| 78 |
+} |
|
| 79 |
+ |
|
| 80 |
+func (l *LocalGroupLister) ListGroups() (ldapGroupUIDs []string, err error) {
|
|
| 81 |
+ groups, err := getOpenShiftGroups(l.whitelist, l.client) |
|
| 82 |
+ if err != nil {
|
|
| 83 |
+ return nil, err |
|
| 84 |
+ } |
|
| 85 |
+ |
|
| 86 |
+ for _, group := range groups {
|
|
| 87 |
+ if err := validateGroupAnnotations(group); err != nil {
|
|
| 88 |
+ return nil, err |
|
| 89 |
+ } |
|
| 90 |
+ ldapGroupUIDs = append(ldapGroupUIDs, group.Annotations[ldaputil.LDAPUIDAnnotation]) |
|
| 91 |
+ } |
|
| 92 |
+ return ldapGroupUIDs, err |
|
| 93 |
+} |
|
| 94 |
+ |
|
| 95 |
+// getOpenShiftGroups uses a client to retrieve all groups from the names given |
|
| 96 |
+func getOpenShiftGroups(names []string, client osclient.GroupInterface) ([]ouserapi.Group, error) {
|
|
| 97 |
+ var groups []ouserapi.Group |
|
| 98 |
+ for _, name := range names {
|
|
| 99 |
+ group, err := client.Get(name) |
|
| 100 |
+ if err != nil {
|
|
| 101 |
+ return nil, err |
|
| 102 |
+ } |
|
| 103 |
+ groups = append(groups, *group) |
|
| 104 |
+ } |
|
| 105 |
+ return groups, nil |
|
| 106 |
+} |
|
| 107 |
+ |
|
| 108 |
+// NewLDAPWhitelistGroupLister returns a new WhitelistLDAPGroupLister that divulges the given whitelist |
|
| 109 |
+// of LDAP group unique identifiers |
|
| 110 |
+func NewLDAPWhitelistGroupLister(whitelist []string) interfaces.LDAPGroupLister {
|
|
| 111 |
+ return &WhitelistLDAPGroupLister{
|
|
| 112 |
+ GroupUIDs: whitelist, |
|
| 113 |
+ } |
|
| 114 |
+} |
|
| 115 |
+ |
|
| 116 |
+// LDAPGroupLister lists LDAP groups unique group identifiers given to it upon creation. |
|
| 117 |
+type WhitelistLDAPGroupLister struct {
|
|
| 118 |
+ GroupUIDs []string |
|
| 119 |
+} |
|
| 120 |
+ |
|
| 121 |
+func (l *WhitelistLDAPGroupLister) ListGroups() (ldapGroupUIDs []string, err error) {
|
|
| 122 |
+ return l.GroupUIDs, nil |
|
| 123 |
+} |
| 0 | 124 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,59 @@ |
| 0 |
+package syncgroups |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "fmt" |
|
| 4 |
+ |
|
| 5 |
+ "github.com/openshift/origin/pkg/auth/ldaputil" |
|
| 6 |
+ "github.com/openshift/origin/pkg/cmd/experimental/syncgroups/interfaces" |
|
| 7 |
+) |
|
| 8 |
+ |
|
| 9 |
+// NewUserDefinedGroupNameMapper returns a new UserDefinedLDAPGroupNameMapper which maps a ldapGroupUID |
|
| 10 |
+// representing an LDAP group to the OpenShift Group name for the resource |
|
| 11 |
+func NewUserDefinedGroupNameMapper(mapping map[string]string) interfaces.LDAPGroupNameMapper {
|
|
| 12 |
+ return &UserDefinedLDAPGroupNameMapper{
|
|
| 13 |
+ nameMapping: mapping, |
|
| 14 |
+ } |
|
| 15 |
+} |
|
| 16 |
+ |
|
| 17 |
+// UserDefinedLDAPGroupNameMapper maps a ldapGroupUID representing an LDAP group to the OpenShift Group |
|
| 18 |
+// name for the resource by using a pre-defined mapping of ldapGroupUID to name (e.g. from a file) |
|
| 19 |
+type UserDefinedLDAPGroupNameMapper struct {
|
|
| 20 |
+ nameMapping map[string]string |
|
| 21 |
+} |
|
| 22 |
+ |
|
| 23 |
+func (m *UserDefinedLDAPGroupNameMapper) GroupNameFor(ldapGroupUID string) (string, error) {
|
|
| 24 |
+ openShiftGroupName, exists := m.nameMapping[ldapGroupUID] |
|
| 25 |
+ if !exists {
|
|
| 26 |
+ return "", fmt.Errorf("no OpenShift Group name defined for LDAP group UID: %s", ldapGroupUID)
|
|
| 27 |
+ } |
|
| 28 |
+ return openShiftGroupName, nil |
|
| 29 |
+} |
|
| 30 |
+ |
|
| 31 |
+// NewEntryAttributeGroupNameMapper returns a new EntryAttributeLDAPGroupNameMapper |
|
| 32 |
+func NewEntryAttributeGroupNameMapper(nameAttribute []string, |
|
| 33 |
+ groupGetter interfaces.LDAPGroupGetter) interfaces.LDAPGroupNameMapper {
|
|
| 34 |
+ return &EntryAttributeLDAPGroupNameMapper{
|
|
| 35 |
+ nameAttribute: nameAttribute, |
|
| 36 |
+ groupGetter: groupGetter, |
|
| 37 |
+ } |
|
| 38 |
+} |
|
| 39 |
+ |
|
| 40 |
+// EntryAttributeLDAPGroupNameMapper references the name attribute mapping to determine which attribute |
|
| 41 |
+// of a first-class LDAP group entry should be used as the OpenShift Group name for the resource |
|
| 42 |
+type EntryAttributeLDAPGroupNameMapper struct {
|
|
| 43 |
+ nameAttribute []string |
|
| 44 |
+ groupGetter interfaces.LDAPGroupGetter |
|
| 45 |
+} |
|
| 46 |
+ |
|
| 47 |
+func (m *EntryAttributeLDAPGroupNameMapper) GroupNameFor(ldapGroupUID string) (string, error) {
|
|
| 48 |
+ group, err := m.groupGetter.GroupEntryFor(ldapGroupUID) |
|
| 49 |
+ if err != nil {
|
|
| 50 |
+ return "", err |
|
| 51 |
+ } |
|
| 52 |
+ openShiftGroupName := ldaputil.GetAttributeValue(group, m.nameAttribute) |
|
| 53 |
+ if len(openShiftGroupName) == 0 {
|
|
| 54 |
+ return "", fmt.Errorf("the group entry (%v) does not map to an OpenShift Group name with the given name attribute (%v)",
|
|
| 55 |
+ group, m.nameAttribute) |
|
| 56 |
+ } |
|
| 57 |
+ return openShiftGroupName, nil |
|
| 58 |
+} |
| 0 | 59 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,141 @@ |
| 0 |
+package syncgroups |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "fmt" |
|
| 4 |
+ "time" |
|
| 5 |
+ |
|
| 6 |
+ "github.com/go-ldap/ldap" |
|
| 7 |
+ |
|
| 8 |
+ "github.com/openshift/origin/pkg/auth/ldaputil" |
|
| 9 |
+ "github.com/openshift/origin/pkg/client" |
|
| 10 |
+ "github.com/openshift/origin/pkg/cmd/experimental/syncgroups/interfaces" |
|
| 11 |
+ ouserapi "github.com/openshift/origin/pkg/user/api" |
|
| 12 |
+) |
|
| 13 |
+ |
|
| 14 |
+// GroupSyncer runs a Sync job on Groups |
|
| 15 |
+type GroupSyncer interface {
|
|
| 16 |
+ Sync() (errors []error) |
|
| 17 |
+} |
|
| 18 |
+ |
|
| 19 |
+// LDAPGroupSyncer sync Groups with records on an external LDAP server |
|
| 20 |
+type LDAPGroupSyncer struct {
|
|
| 21 |
+ // Lists all groups to be synced |
|
| 22 |
+ GroupLister interfaces.LDAPGroupLister |
|
| 23 |
+ // Fetches a group and extracts object metainformation and membership list from a group |
|
| 24 |
+ GroupMemberExtractor interfaces.LDAPMemberExtractor |
|
| 25 |
+ // Maps an LDAP user entry to an OpenShift User's Name |
|
| 26 |
+ UserNameMapper interfaces.LDAPUserNameMapper |
|
| 27 |
+ // Maps an LDAP group enrty to an OpenShift Group's Name |
|
| 28 |
+ GroupNameMapper interfaces.LDAPGroupNameMapper |
|
| 29 |
+ // Allows the Syncer to search for OpenShift Groups |
|
| 30 |
+ GroupClient client.GroupInterface |
|
| 31 |
+ // Host stores the address:port of the LDAP server |
|
| 32 |
+ Host string |
|
| 33 |
+} |
|
| 34 |
+ |
|
| 35 |
+// Sync allows the LDAPGroupSyncer to be a GroupSyncer |
|
| 36 |
+func (s *LDAPGroupSyncer) Sync() []error {
|
|
| 37 |
+ var errors []error |
|
| 38 |
+ // determine what to sync |
|
| 39 |
+ ldapGroupUIDs, err := s.GroupLister.ListGroups() |
|
| 40 |
+ if err != nil {
|
|
| 41 |
+ errors = append(errors, err) |
|
| 42 |
+ return errors |
|
| 43 |
+ } |
|
| 44 |
+ |
|
| 45 |
+ for _, ldapGroupUID := range ldapGroupUIDs {
|
|
| 46 |
+ // get membership data |
|
| 47 |
+ memberEntries, err := s.GroupMemberExtractor.ExtractMembers(ldapGroupUID) |
|
| 48 |
+ if err != nil {
|
|
| 49 |
+ errors = append(errors, err) |
|
| 50 |
+ continue |
|
| 51 |
+ } |
|
| 52 |
+ |
|
| 53 |
+ // determine OpenShift Users' usernames for LDAP group members |
|
| 54 |
+ usernames, err := s.determineUsernames(memberEntries) |
|
| 55 |
+ if err != nil {
|
|
| 56 |
+ errors = append(errors, err) |
|
| 57 |
+ continue |
|
| 58 |
+ } |
|
| 59 |
+ |
|
| 60 |
+ // update the OpenShift Group corresponding to this record |
|
| 61 |
+ err = s.updateGroup(ldapGroupUID, usernames) |
|
| 62 |
+ if err != nil {
|
|
| 63 |
+ errors = append(errors, err) |
|
| 64 |
+ } |
|
| 65 |
+ |
|
| 66 |
+ } |
|
| 67 |
+ return errors |
|
| 68 |
+} |
|
| 69 |
+ |
|
| 70 |
+// determineUsers determines the OpenShift Users that correspond to a list of LDAP member entries |
|
| 71 |
+func (s *LDAPGroupSyncer) determineUsernames(members []*ldap.Entry) ([]string, error) {
|
|
| 72 |
+ var usernames []string |
|
| 73 |
+ for _, member := range members {
|
|
| 74 |
+ username, err := s.UserNameMapper.UserNameFor(member) |
|
| 75 |
+ if err != nil {
|
|
| 76 |
+ return nil, err |
|
| 77 |
+ } |
|
| 78 |
+ usernames = append(usernames, username) |
|
| 79 |
+ } |
|
| 80 |
+ return usernames, nil |
|
| 81 |
+} |
|
| 82 |
+ |
|
| 83 |
+// updateGroup finds or creates the OpenShift Group that needs to be updated, updates its' data, then |
|
| 84 |
+// uses the GroupClient to update the Group record |
|
| 85 |
+func (s *LDAPGroupSyncer) updateGroup(ldapGroupUID string, usernames []string) error {
|
|
| 86 |
+ // find OpenShift Group to update |
|
| 87 |
+ group, err := s.findGroup(ldapGroupUID) |
|
| 88 |
+ if err != nil {
|
|
| 89 |
+ return err |
|
| 90 |
+ } |
|
| 91 |
+ |
|
| 92 |
+ // overwrite Group Users data |
|
| 93 |
+ group.Users = usernames |
|
| 94 |
+ |
|
| 95 |
+ // add LDAP-sync-specific annotations |
|
| 96 |
+ group.Annotations[ldaputil.LDAPUIDAnnotation] = ldapGroupUID |
|
| 97 |
+ group.Annotations[ldaputil.LDAPSyncTimeAnnotation] = ISO8601(time.Now()) |
|
| 98 |
+ group.Annotations[ldaputil.LDAPURLAnnotation] = s.Host |
|
| 99 |
+ |
|
| 100 |
+ _, err = s.GroupClient.Update(group) |
|
| 101 |
+ return err |
|
| 102 |
+} |
|
| 103 |
+ |
|
| 104 |
+// findGroup finds the OpenShift Group for the LDAP group UID and ensures that the OpenShift Group found |
|
| 105 |
+// was created as a result of a previous LDAP sync from the same LDAP group. |
|
| 106 |
+func (s *LDAPGroupSyncer) findGroup(ldapGroupUID string) (*ouserapi.Group, error) {
|
|
| 107 |
+ groupName, err := s.GroupNameMapper.GroupNameFor(ldapGroupUID) |
|
| 108 |
+ if err != nil {
|
|
| 109 |
+ return nil, err |
|
| 110 |
+ } |
|
| 111 |
+ |
|
| 112 |
+ group, err := s.GroupClient.Get(groupName) |
|
| 113 |
+ if err != nil {
|
|
| 114 |
+ return nil, fmt.Errorf("could not get group for name: %s", groupName)
|
|
| 115 |
+ } |
|
| 116 |
+ |
|
| 117 |
+ url, exists := group.Annotations[ldaputil.LDAPURLAnnotation] |
|
| 118 |
+ if !exists || url != s.Host {
|
|
| 119 |
+ return nil, fmt.Errorf("group %s's %s annotation did not match sync host: wanted %s, got %s",
|
|
| 120 |
+ group.Name, ldaputil.LDAPURLAnnotation, s.Host, url) |
|
| 121 |
+ } |
|
| 122 |
+ uid, exists := group.Annotations[ldaputil.LDAPUIDAnnotation] |
|
| 123 |
+ if !exists || uid != ldapGroupUID {
|
|
| 124 |
+ return nil, fmt.Errorf("group %s's %s annotation did not match sync host: wanted %s, got %s",
|
|
| 125 |
+ group.Name, ldaputil.LDAPUIDAnnotation, ldapGroupUID, uid) |
|
| 126 |
+ } |
|
| 127 |
+ return group, nil |
|
| 128 |
+} |
|
| 129 |
+ |
|
| 130 |
+// ISO8601 returns an ISO 6801 formatted string from a time. |
|
| 131 |
+func ISO8601(t time.Time) string {
|
|
| 132 |
+ var tz string |
|
| 133 |
+ if zone, offset := t.Zone(); zone == "UTC" {
|
|
| 134 |
+ tz = "Z" |
|
| 135 |
+ } else {
|
|
| 136 |
+ tz = fmt.Sprintf("%03d00", offset/3600)
|
|
| 137 |
+ } |
|
| 138 |
+ return fmt.Sprintf("%04d-%02d-%02dT%02d:%02d:%02d%s",
|
|
| 139 |
+ t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), tz) |
|
| 140 |
+} |
| 0 | 141 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,32 @@ |
| 0 |
+package interfaces |
|
| 1 |
+ |
|
| 2 |
+import "github.com/go-ldap/ldap" |
|
| 3 |
+ |
|
| 4 |
+// LDAPGroupLister lists the LDAP groups that need to be synced by a job. The LDAPGroupLister needs to |
|
| 5 |
+// be paired with an LDAPMemberExtractor that understands the format of the unique identifiers returned |
|
| 6 |
+// to represent the LDAP groups to be synced. |
|
| 7 |
+type LDAPGroupLister interface {
|
|
| 8 |
+ ListGroups() (ldapGroupUIDs []string, err error) |
|
| 9 |
+} |
|
| 10 |
+ |
|
| 11 |
+// LDAPMemberExtractor retrieves member data about an LDAP group from the LDAP server. |
|
| 12 |
+type LDAPMemberExtractor interface {
|
|
| 13 |
+ // ExtractMembers returns the list of LDAP first-class user entries that are members of the LDAP group |
|
| 14 |
+ // specified by the ldapGroupUID |
|
| 15 |
+ ExtractMembers(ldapGroupUID string) (members []*ldap.Entry, err error) |
|
| 16 |
+} |
|
| 17 |
+ |
|
| 18 |
+// LDAPGroupNameMapper maps a ldapGroupUID representing an LDAP group to the OpenShift Group name for the resource |
|
| 19 |
+type LDAPGroupNameMapper interface {
|
|
| 20 |
+ GroupNameFor(ldapGroupUID string) (openShiftGroupName string, err error) |
|
| 21 |
+} |
|
| 22 |
+ |
|
| 23 |
+// LDAPUserNameMapper maps an LDAP entry representing an LDAP user to the OpenShift User name for the resource |
|
| 24 |
+type LDAPUserNameMapper interface {
|
|
| 25 |
+ UserNameFor(ldapUser *ldap.Entry) (openShiftUserName string, err error) |
|
| 26 |
+} |
|
| 27 |
+ |
|
| 28 |
+// LDAPGroupGetter maps a ldapGroupUID to a first-class LDAP group entry |
|
| 29 |
+type LDAPGroupGetter interface {
|
|
| 30 |
+ GroupEntryFor(ldapGroupUID string) (group *ldap.Entry, err error) |
|
| 31 |
+} |
| 0 | 32 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,154 @@ |
| 0 |
+package rfc2307 |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "github.com/go-ldap/ldap" |
|
| 4 |
+ |
|
| 5 |
+ "github.com/openshift/origin/pkg/auth/ldaputil" |
|
| 6 |
+) |
|
| 7 |
+ |
|
| 8 |
+// NewLDAPInterface builds a new LDAPInterface using a schema-appropriate config |
|
| 9 |
+func NewLDAPInterface(clientConfig ldaputil.LDAPClientConfig, |
|
| 10 |
+ groupQuery ldaputil.IdentifiyingLDAPQueryOptions, |
|
| 11 |
+ userQuery ldaputil.IdentifiyingLDAPQueryOptions, |
|
| 12 |
+ groupMembershipAttributes []string) LDAPInterface {
|
|
| 13 |
+ return LDAPInterface{
|
|
| 14 |
+ clientConfig: clientConfig, |
|
| 15 |
+ groupQuery: groupQuery, |
|
| 16 |
+ userQuery: userQuery, |
|
| 17 |
+ groupMembershipAttributes: groupMembershipAttributes, |
|
| 18 |
+ cachedUsers: make(map[string]*ldap.Entry), |
|
| 19 |
+ cachedGroups: make(map[string]*ldap.Entry), |
|
| 20 |
+ } |
|
| 21 |
+} |
|
| 22 |
+ |
|
| 23 |
+// LDAPInterface extracts the member list of an LDAP group entry from an LDAP server |
|
| 24 |
+// with first-class LDAP entries for groups. The LDAPInterface is *NOT* thread-safe. |
|
| 25 |
+// The LDAPInterface satisfies: |
|
| 26 |
+// - LDAPMemberExtractor |
|
| 27 |
+// - LDAPGroupGetter |
|
| 28 |
+// - LDAPGroupLister |
|
| 29 |
+type LDAPInterface struct {
|
|
| 30 |
+ // clientConfig holds LDAP connection information |
|
| 31 |
+ clientConfig ldaputil.LDAPClientConfig |
|
| 32 |
+ // groupQuery holds the information necessary to make an LDAP query for a specific |
|
| 33 |
+ // first-class group entry on the LDAP server |
|
| 34 |
+ groupQuery ldaputil.IdentifiyingLDAPQueryOptions |
|
| 35 |
+ // userQuery holds the information necessary to make an LDAP query for a specific |
|
| 36 |
+ // first-class user entry on the LDAP server |
|
| 37 |
+ userQuery ldaputil.IdentifiyingLDAPQueryOptions |
|
| 38 |
+ // groupMembershipAttributes defines which attributes on an LDAP user entry will be interpreted |
|
| 39 |
+ // as the groups it is a member of |
|
| 40 |
+ groupMembershipAttributes []string |
|
| 41 |
+ |
|
| 42 |
+ // cachedGroups holds the result of group queries for later reference, indexed on group UID |
|
| 43 |
+ // e.g. this will map an LDAP group UID to the LDAP entry returned from the query made using it |
|
| 44 |
+ cachedGroups map[string]*ldap.Entry |
|
| 45 |
+ // cachedUsers holds the result of user queries for later reference, indexed on user UID |
|
| 46 |
+ // e.g. this will map an LDAP user UID to the LDAP entry returned from the query made using it |
|
| 47 |
+ cachedUsers map[string]*ldap.Entry |
|
| 48 |
+} |
|
| 49 |
+ |
|
| 50 |
+// ExtractMembers returns the LDAP member entries for a group specified with a ldapGroupUID |
|
| 51 |
+func (e *LDAPInterface) ExtractMembers(ldapGroupUID string) (members []*ldap.Entry, err error) {
|
|
| 52 |
+ // get group entry from LDAP |
|
| 53 |
+ group, err := e.GroupEntryFor(ldapGroupUID) |
|
| 54 |
+ if err != nil {
|
|
| 55 |
+ return nil, err |
|
| 56 |
+ } |
|
| 57 |
+ |
|
| 58 |
+ // extract member UIDs from group entry |
|
| 59 |
+ var ldapMemberUIDs []string |
|
| 60 |
+ for _, attribute := range e.userQuery.NameAttributes {
|
|
| 61 |
+ ldapMemberUIDs = append(ldapMemberUIDs, group.GetAttributeValues(attribute)...) |
|
| 62 |
+ } |
|
| 63 |
+ |
|
| 64 |
+ // find members on LDAP server or in cache |
|
| 65 |
+ for _, ldapMemberUID := range ldapMemberUIDs {
|
|
| 66 |
+ memberEntry, err := e.userEntryFor(ldapMemberUID) |
|
| 67 |
+ if err != nil {
|
|
| 68 |
+ return nil, err |
|
| 69 |
+ } |
|
| 70 |
+ members = append(members, memberEntry) |
|
| 71 |
+ } |
|
| 72 |
+ return members, nil |
|
| 73 |
+} |
|
| 74 |
+ |
|
| 75 |
+// GroupFor returns an LDAP group entry for the given group UID by searching the internal cache |
|
| 76 |
+// of the LDAPInterface first, then sending an LDAP query if the cache did not contain the entry. |
|
| 77 |
+// This also satisfies the LDAPGroupGetter interface |
|
| 78 |
+func (e *LDAPInterface) GroupEntryFor(ldapGroupUID string) (group *ldap.Entry, err error) {
|
|
| 79 |
+ group, exists := e.cachedGroups[ldapGroupUID] |
|
| 80 |
+ if !exists {
|
|
| 81 |
+ group, err = e.queryForGroup(ldapGroupUID) |
|
| 82 |
+ if err != nil {
|
|
| 83 |
+ return nil, err |
|
| 84 |
+ } |
|
| 85 |
+ // cache for annotation extraction |
|
| 86 |
+ e.cachedGroups[ldapGroupUID] = group |
|
| 87 |
+ } |
|
| 88 |
+ return group, nil |
|
| 89 |
+} |
|
| 90 |
+ |
|
| 91 |
+// queryForGroup queries for a specific group identified by a ldapGroupUID with the query config stored |
|
| 92 |
+// in a LDAPInterface |
|
| 93 |
+func (e *LDAPInterface) queryForGroup(ldapGroupUID string) (group *ldap.Entry, err error) {
|
|
| 94 |
+ // create the search request |
|
| 95 |
+ searchRequest, err := e.groupQuery.NewSearchRequest(ldapGroupUID, e.groupMembershipAttributes) |
|
| 96 |
+ if err != nil {
|
|
| 97 |
+ return nil, err |
|
| 98 |
+ } |
|
| 99 |
+ |
|
| 100 |
+ return ldaputil.QueryForUniqueEntry(e.clientConfig, searchRequest) |
|
| 101 |
+} |
|
| 102 |
+ |
|
| 103 |
+// userEntryFor returns an LDAP group entry for the given group UID by searching the internal cache |
|
| 104 |
+// of the LDAPInterface first, then sending an LDAP query if the cache did not contain the entry |
|
| 105 |
+func (e *LDAPInterface) userEntryFor(ldapUserUID string) (user *ldap.Entry, err error) {
|
|
| 106 |
+ user, exists := e.cachedUsers[ldapUserUID] |
|
| 107 |
+ if !exists {
|
|
| 108 |
+ user, err = e.queryForUser(ldapUserUID) |
|
| 109 |
+ if err != nil {
|
|
| 110 |
+ return nil, err |
|
| 111 |
+ } |
|
| 112 |
+ // cache for annotation extraction |
|
| 113 |
+ e.cachedUsers[ldapUserUID] = user |
|
| 114 |
+ } |
|
| 115 |
+ return user, nil |
|
| 116 |
+} |
|
| 117 |
+ |
|
| 118 |
+// queryForUser queries for an LDAP user entry identified with an LDAP user UID on an LDAP server |
|
| 119 |
+// determined from a clientConfig by creating a search request from an LDAP query template and |
|
| 120 |
+// determining which attributes to search for with a LDAPuserAttributeDefiner |
|
| 121 |
+func (e *LDAPInterface) queryForUser(ldapUserUID string) (user *ldap.Entry, err error) {
|
|
| 122 |
+ // create the search request |
|
| 123 |
+ searchRequest, err := e.userQuery.NewSearchRequest(ldapUserUID, []string{})
|
|
| 124 |
+ if err != nil {
|
|
| 125 |
+ return nil, err |
|
| 126 |
+ } |
|
| 127 |
+ |
|
| 128 |
+ return ldaputil.QueryForUniqueEntry(e.clientConfig, searchRequest) |
|
| 129 |
+} |
|
| 130 |
+ |
|
| 131 |
+// ListGroups queries for all groups as configured with the common group filter and returns their |
|
| 132 |
+// LDAP group UIDs. This also satisfies the LDAPGroupLister interface |
|
| 133 |
+func (e *LDAPInterface) ListGroups() (ldapGroupUIDs []string, err error) {
|
|
| 134 |
+ groups, err := e.queryForGroups() |
|
| 135 |
+ if err != nil {
|
|
| 136 |
+ return nil, err |
|
| 137 |
+ } |
|
| 138 |
+ for _, group := range groups {
|
|
| 139 |
+ // cache groups returned from the server for later |
|
| 140 |
+ ldapGroupUID := ldaputil.GetAttributeValue(group, e.groupQuery.NameAttributes) |
|
| 141 |
+ e.cachedGroups[ldapGroupUID] = group |
|
| 142 |
+ ldapGroupUIDs = append(ldapGroupUIDs, ldapGroupUID) |
|
| 143 |
+ } |
|
| 144 |
+ return ldapGroupUIDs, nil |
|
| 145 |
+} |
|
| 146 |
+ |
|
| 147 |
+// queryForGroups queries for all groups identified by a common filter in the query config stored |
|
| 148 |
+// in a GroupListerDataExtractor |
|
| 149 |
+func (e *LDAPInterface) queryForGroups() (groups []*ldap.Entry, err error) {
|
|
| 150 |
+ // create the search request |
|
| 151 |
+ searchRequest := e.groupQuery.LDAPQuery.NewSearchRequest(e.groupMembershipAttributes) |
|
| 152 |
+ return ldaputil.QueryForEntries(e.clientConfig, searchRequest) |
|
| 153 |
+} |
| 0 | 154 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,7 @@ |
| 0 |
+package syncgroups |
|
| 1 |
+ |
|
| 2 |
+import "github.com/openshift/origin/pkg/cmd/experimental/syncgroups/interfaces" |
|
| 3 |
+ |
|
| 4 |
+var _ interfaces.LDAPMemberExtractor = &LDAPInterface{}
|
|
| 5 |
+var _ interfaces.LDAPGroupGetter = &LDAPInterface{}
|
|
| 6 |
+var _ interfaces.LDAPGroupLister = &LDAPInterface{}
|
| 0 | 7 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,32 @@ |
| 0 |
+package syncgroups |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "fmt" |
|
| 4 |
+ |
|
| 5 |
+ "github.com/go-ldap/ldap" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/openshift/origin/pkg/auth/ldaputil" |
|
| 8 |
+ "github.com/openshift/origin/pkg/cmd/experimental/syncgroups/interfaces" |
|
| 9 |
+) |
|
| 10 |
+ |
|
| 11 |
+// NewUserNameMapper returns a new DefaultLDAPGroupUserNameMapper |
|
| 12 |
+func NewUserNameMapper(nameAttributes []string) interfaces.LDAPUserNameMapper {
|
|
| 13 |
+ return &DefaultLDAPUserNameMapper{
|
|
| 14 |
+ nameAttributes: nameAttributes, |
|
| 15 |
+ } |
|
| 16 |
+} |
|
| 17 |
+ |
|
| 18 |
+// DefaultLDAPUserNameMapper extracts the OpenShift User name of an LDAP entry representing |
|
| 19 |
+// a user in a deterministic manner |
|
| 20 |
+type DefaultLDAPUserNameMapper struct {
|
|
| 21 |
+ nameAttributes []string |
|
| 22 |
+} |
|
| 23 |
+ |
|
| 24 |
+func (m *DefaultLDAPUserNameMapper) UserNameFor(ldapUser *ldap.Entry) (openShiftUserName string, err error) {
|
|
| 25 |
+ openShiftUserName = ldaputil.GetAttributeValue(ldapUser, m.nameAttributes) |
|
| 26 |
+ if len(openShiftUserName) == 0 {
|
|
| 27 |
+ return "", fmt.Errorf("the user entry (%v) does not map to a OpenShift User name with the given mapping",
|
|
| 28 |
+ ldapUser) |
|
| 29 |
+ } |
|
| 30 |
+ return openShiftUserName, nil |
|
| 31 |
+} |
| ... | ... |
@@ -23,6 +23,8 @@ func init() {
|
| 23 | 23 |
&GoogleIdentityProvider{},
|
| 24 | 24 |
&OpenIDIdentityProvider{},
|
| 25 | 25 |
&GrantConfig{},
|
| 26 |
+ |
|
| 27 |
+ &LDAPSyncConfig{},
|
|
| 26 | 28 |
) |
| 27 | 29 |
} |
| 28 | 30 |
|
| ... | ... |
@@ -41,3 +43,5 @@ func (*GrantConfig) IsAnAPIObject() {}
|
| 41 | 41 |
func (*MasterConfig) IsAnAPIObject() {}
|
| 42 | 42 |
func (*NodeConfig) IsAnAPIObject() {}
|
| 43 | 43 |
func (*SessionSecrets) IsAnAPIObject() {}
|
| 44 |
+ |
|
| 45 |
+func (*LDAPSyncConfig) IsAnAPIObject() {}
|
| ... | ... |
@@ -712,3 +712,130 @@ type AssetExtensionsConfig struct {
|
| 712 | 712 |
// Web Console's context root. Defaults to false. |
| 713 | 713 |
HTML5Mode bool |
| 714 | 714 |
} |
| 715 |
+ |
|
| 716 |
+type LDAPSyncConfig struct {
|
|
| 717 |
+ api.TypeMeta |
|
| 718 |
+ // Host is the scheme, host and port of the LDAP server to connect to: |
|
| 719 |
+ // scheme://host:port |
|
| 720 |
+ Host string |
|
| 721 |
+ // BindDN is an optional DN to bind with during the search phase. |
|
| 722 |
+ BindDN string |
|
| 723 |
+ // BindPassword is an optional password to bind with during the search phase. |
|
| 724 |
+ BindPassword string |
|
| 725 |
+ // Insecure, if true, indicates the connection should not use TLS. |
|
| 726 |
+ // Cannot be set to true with a URL scheme of "ldaps://" |
|
| 727 |
+ // If false, "ldaps://" URLs connect using TLS, and "ldap://" URLs are upgraded to a TLS connection using StartTLS as specified in https://tools.ietf.org/html/rfc2830 |
|
| 728 |
+ Insecure bool |
|
| 729 |
+ // CA is the optional trusted certificate authority bundle to use when making requests to the server |
|
| 730 |
+ // If empty, the default system roots are used |
|
| 731 |
+ CA string |
|
| 732 |
+ |
|
| 733 |
+ // LDAPGroupUIDToOpenShiftGroupNameMapping is an optional direct mapping of LDAP group UIDs to |
|
| 734 |
+ // OpenShift Group names |
|
| 735 |
+ LDAPGroupUIDToOpenShiftGroupNameMapping map[string]string |
|
| 736 |
+ |
|
| 737 |
+ // LDAPSchemaSpecificConfig holds the configuration for retrieving data from the LDAP server. |
|
| 738 |
+ // This set of configuration varies with LDAP server schema. |
|
| 739 |
+ LDAPSchemaSpecificConfig |
|
| 740 |
+} |
|
| 741 |
+ |
|
| 742 |
+// LDAPSchemaSpecificConfig holds the schema-specific configuration for data retrieval from the LDAP |
|
| 743 |
+// server. Only one of the members can be specified. |
|
| 744 |
+type LDAPSchemaSpecificConfig struct {
|
|
| 745 |
+ // RFC2307Config holds the configuration for extracting data from an LDAP server set up in a fashion |
|
| 746 |
+ // similar to RFC2307: first-class group and user entries, with group membership determined by a |
|
| 747 |
+ // multi-valued attribute on the group entry listing its' members |
|
| 748 |
+ RFC2307Config *RFC2307Config |
|
| 749 |
+ |
|
| 750 |
+ // ActiveDirectoryConfig holds the configuration for extracting data from an LDAP server set up in a |
|
| 751 |
+ // fashion similar to that used in Active Directory: first-class user entries, with group membership |
|
| 752 |
+ // determined by a multi-valued attribute on members listing groups they are a member of |
|
| 753 |
+ ActiveDirectoryConfig *ActiveDirectoryConfig |
|
| 754 |
+ |
|
| 755 |
+ // AugmentedActiveDirectoryConfig holds the configuration for extracting data from an LDAP server |
|
| 756 |
+ // set up in a fashion similar to that used in Active Directory as described above, with one addition: |
|
| 757 |
+ // first-class group entries exist and are used to hold metadata but not group membership |
|
| 758 |
+ AugmentedActiveDirectoryConfig *AugmentedActiveDirectoryConfig |
|
| 759 |
+} |
|
| 760 |
+ |
|
| 761 |
+type RFC2307Config struct {
|
|
| 762 |
+ // GroupQuery holds the template for an LDAP query that returns group entries |
|
| 763 |
+ GroupQuery LDAPQuery |
|
| 764 |
+ |
|
| 765 |
+ // GroupNameAttributes defines which attributes on an LDAP group entry will be interpreted as its' name |
|
| 766 |
+ GroupNameAttributes []string |
|
| 767 |
+ |
|
| 768 |
+ // GroupMembershipAttributes defines which attributes on an LDAP group entry will be interpreted |
|
| 769 |
+ // as its' members |
|
| 770 |
+ GroupMembershipAttributes []string |
|
| 771 |
+ |
|
| 772 |
+ // UserQuery holds the template for an LDAP query that returns user entries |
|
| 773 |
+ UserQuery LDAPQuery |
|
| 774 |
+ |
|
| 775 |
+ // UserNameAttributes defines which attributes on an LDAP user entry will be interpreted as its' name |
|
| 776 |
+ UserNameAttributes []string |
|
| 777 |
+} |
|
| 778 |
+ |
|
| 779 |
+type ActiveDirectoryConfig struct {
|
|
| 780 |
+ // UsersQuery holds the template for an LDAP query that returns all user entries |
|
| 781 |
+ // that are labelled as being members of a group |
|
| 782 |
+ UsersQuery LDAPQuery |
|
| 783 |
+ |
|
| 784 |
+ // UserNameAttributes defines which attributes on an LDAP user entry will be interpreted as its' name |
|
| 785 |
+ UserNameAttributes []string |
|
| 786 |
+ |
|
| 787 |
+ // GroupMembershipAttributes defines which attributes on an LDAP user entry will be interpreted |
|
| 788 |
+ // as the groups it is a member of |
|
| 789 |
+ GroupMembershipAttributes []string |
|
| 790 |
+} |
|
| 791 |
+ |
|
| 792 |
+type AugmentedActiveDirectoryConfig struct {
|
|
| 793 |
+ // GroupQuery holds the template for an LDAP query that returns group entries |
|
| 794 |
+ GroupQuery LDAPQuery |
|
| 795 |
+ |
|
| 796 |
+ // GroupNameAttributes defines which attributes on an LDAP group entry will be interpreted as its' name |
|
| 797 |
+ GroupNameAttributes []string |
|
| 798 |
+ |
|
| 799 |
+ // UsersQuery holds the template for an LDAP query that returns all user entries |
|
| 800 |
+ // that are labelled as being members of a group |
|
| 801 |
+ UsersQuery LDAPQuery |
|
| 802 |
+ |
|
| 803 |
+ // UserNameAttributes defines which attributes on an LDAP user entry will be interpreted as its' name |
|
| 804 |
+ UserNameAttributes []string |
|
| 805 |
+ |
|
| 806 |
+ // GroupMembershipAttributes defines which attributes on an LDAP user entry will be interpreted |
|
| 807 |
+ // as the groups it is a member of |
|
| 808 |
+ GroupMembershipAttributes []string |
|
| 809 |
+} |
|
| 810 |
+ |
|
| 811 |
+type LDAPQuery struct {
|
|
| 812 |
+ // The DN of the branch of the directory where all searches should start from |
|
| 813 |
+ BaseDN string |
|
| 814 |
+ |
|
| 815 |
+ // The (optional) scope of the search. Can be: |
|
| 816 |
+ // base: only the base object, |
|
| 817 |
+ // one: all object on the base level, |
|
| 818 |
+ // sub: the entire subtree |
|
| 819 |
+ // Defaults to the entire subtree if not set |
|
| 820 |
+ Scope string |
|
| 821 |
+ |
|
| 822 |
+ // The (optional) behavior of the search with regards to alisases. Can be: |
|
| 823 |
+ // never: never dereference aliases, |
|
| 824 |
+ // search: only dereference in searching, |
|
| 825 |
+ // base: only dereference in finding the base object, |
|
| 826 |
+ // always: always dereference |
|
| 827 |
+ // Defaults to always dereferencing if not set |
|
| 828 |
+ DerefAliases string |
|
| 829 |
+ |
|
| 830 |
+ // TimeLimit holds the limit of time in seconds that any request to the server can remain outstanding |
|
| 831 |
+ // before the wait for a response is given up. If this is 0, no client-side limit is imposed |
|
| 832 |
+ TimeLimit int |
|
| 833 |
+ |
|
| 834 |
+ // Filter is a valid LDAP search filter that retrieves all relevant entries from the LDAP server with the base DN |
|
| 835 |
+ Filter string |
|
| 836 |
+ |
|
| 837 |
+ // QueryAttribute is the attribute for a specific filter that, when conjoined with the common filter, |
|
| 838 |
+ // retrieves the specific LDAP entry from the LDAP server. (e.g. "cn", when formatted with "aGroupName" |
|
| 839 |
+ // and conjoined with "objectClass=groupOfNames", becomes (&(objectClass=groupOfNames)(cn=aGroupName))") |
|
| 840 |
+ QueryAttribute string |
|
| 841 |
+} |
| ... | ... |
@@ -1,6 +1,7 @@ |
| 1 | 1 |
package v1 |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 |
+ "k8s.io/kubernetes/pkg/api" |
|
| 4 | 5 |
"k8s.io/kubernetes/pkg/api/v1" |
| 5 | 6 |
"k8s.io/kubernetes/pkg/runtime" |
| 6 | 7 |
) |
| ... | ... |
@@ -696,3 +697,128 @@ type AssetExtensionsConfig struct {
|
| 696 | 696 |
// Web Console's context root. Defaults to false. |
| 697 | 697 |
HTML5Mode bool `json:"html5Mode"` |
| 698 | 698 |
} |
| 699 |
+ |
|
| 700 |
+type LDAPSyncConfig struct {
|
|
| 701 |
+ api.TypeMeta `json:",inline"` |
|
| 702 |
+ // Host is the scheme, host and port of the LDAP server to connect to: |
|
| 703 |
+ // scheme://host:port |
|
| 704 |
+ Host string `json:"host" description:"scheme://host:port for the LDAP server"` |
|
| 705 |
+ // BindDN is an optional DN to bind to the LDAP server with |
|
| 706 |
+ BindDN string `json:"bindDN,omitempty" description:"the optional DN to bind with"` |
|
| 707 |
+ // BindPassword is an optional password to bind with during the search phase. |
|
| 708 |
+ BindPassword string `json:"bindPassword,omitempty" description:"the optional password to bind with"` |
|
| 709 |
+ // Insecure, if true, indicates the connection should not use TLS. |
|
| 710 |
+ // Cannot be set to true with a URL scheme of "ldaps://" |
|
| 711 |
+ // If false, "ldaps://" URLs connect using TLS, and "ldap://" URLs are upgraded to a TLS connection using StartTLS as specified in https://tools.ietf.org/html/rfc2830 |
|
| 712 |
+ Insecure bool `json:"insecure" description:"specifies that the connection with the server should not use TLS"` |
|
| 713 |
+ // CA is the optional trusted certificate authority bundle to use when making requests to the server |
|
| 714 |
+ // If empty, the default system roots are used |
|
| 715 |
+ CA string `json:"CA,omitempty" description:"an optional trusted CA to use when making requests to the server"` |
|
| 716 |
+ |
|
| 717 |
+ // LDAPGroupUIDToOpenShiftGroupNameMapping is an optional direct mapping of LDAP group UIDs to |
|
| 718 |
+ // OpenShift Group names |
|
| 719 |
+ LDAPGroupUIDToOpenShiftGroupNameMapping map[string]string |
|
| 720 |
+ |
|
| 721 |
+ // LDAPSchemaSpecificConfig holds the configuration for retrieving data from the LDAP server. |
|
| 722 |
+ // This set of configuration varies with LDAP server schema. |
|
| 723 |
+ LDAPSchemaSpecificConfig `json:"inline,omitempty" description:"schema-specific LDAP client configuration"` |
|
| 724 |
+} |
|
| 725 |
+ |
|
| 726 |
+// LDAPSchemaSpecificConfig holds the schema-specific configuration for data retrieval from the LDAP |
|
| 727 |
+// server. Only one of the members can be specified. |
|
| 728 |
+type LDAPSchemaSpecificConfig struct {
|
|
| 729 |
+ // RFC2307Config holds the configuration for extracting data from an LDAP server set up in a fashion |
|
| 730 |
+ // similar to RFC2307: first-class group and user entries, with group membership determined by a |
|
| 731 |
+ // multi-valued attribute on the group entry listing its' members |
|
| 732 |
+ RFC2307Config *RFC2307Config `json:"RFC2307,omitempty" description:"schema-specific information for an RFC2307-like schema"` |
|
| 733 |
+ |
|
| 734 |
+ // ActiveDirectoryConfig holds the configuration for extracting data from an LDAP server set up in a |
|
| 735 |
+ // fashion similar to that used in Active Directory: first-class user entries, with group membership |
|
| 736 |
+ // determined by a multi-valued attribute on members listing groups they are a member of |
|
| 737 |
+ ActiveDirectoryConfig *ActiveDirectoryConfig `json:"activeDirectory,omitempty" description:"schema-specific information for an Active Directory-like schema"` |
|
| 738 |
+ |
|
| 739 |
+ // AugmentedActiveDirectoryConfig holds the configuration for extracting data from an LDAP server |
|
| 740 |
+ // set up in a fashion similar to that used in Active Directory as described above, with one addition: |
|
| 741 |
+ // first-class group entries exist and are used to hold metadata but not group membership |
|
| 742 |
+ AugmentedActiveDirectoryConfig *AugmentedActiveDirectoryConfig `json:"augmentedAD,omitempty" description:"schema-specific information for an Active Directory-like schema with group metadata entries"` |
|
| 743 |
+} |
|
| 744 |
+ |
|
| 745 |
+type RFC2307Config struct {
|
|
| 746 |
+ // GroupQuery holds the template for an LDAP query that returns group entries |
|
| 747 |
+ GroupQuery LDAPQuery `json:"groupQuery" description:"the query for a group entry"` |
|
| 748 |
+ |
|
| 749 |
+ // GroupNameAttributes defines which attributes on an LDAP group entry will be interpreted as its' name |
|
| 750 |
+ GroupNameAttributes []string `json:"groupName" description:"the group name attributes"` |
|
| 751 |
+ |
|
| 752 |
+ // GroupMembershipAttributes defines which attributes on an LDAP group entry will be interpreted |
|
| 753 |
+ // as its' members |
|
| 754 |
+ GroupMembershipAttributes []string `json:"groupMembership" description:"the group membership attributes"` |
|
| 755 |
+ |
|
| 756 |
+ // UserQuery holds the template for an LDAP query that returns user entries |
|
| 757 |
+ UserQuery LDAPQuery `json:"userQuery" description:"the query for a user entry"` |
|
| 758 |
+ |
|
| 759 |
+ // UserNameAttributes defines which attributes on an LDAP user entry will be interpreted as its' name |
|
| 760 |
+ UserNameAttributes []string `json:"userName" description:"the user name attributes"` |
|
| 761 |
+} |
|
| 762 |
+ |
|
| 763 |
+type ActiveDirectoryConfig struct {
|
|
| 764 |
+ // UsersQuery holds the template for an LDAP query that returns all user entries that are members of a group |
|
| 765 |
+ UsersQuery LDAPQuery `json:"userQuery" description:"the query for all user entries that are members of a group"` |
|
| 766 |
+ |
|
| 767 |
+ // UserNameAttributes defines which attributes on an LDAP user entry will be interpreted as its' name |
|
| 768 |
+ UserNameAttributes []string `json:"userName" description:"the user name attributes"` |
|
| 769 |
+ |
|
| 770 |
+ // GroupMembershipAttributes defines which attributes on an LDAP user entry will be interpreted |
|
| 771 |
+ // as the groups it is a member of |
|
| 772 |
+ GroupMembershipAttributes []string `json:"groupMembership" description:"the group membership attributes"` |
|
| 773 |
+} |
|
| 774 |
+ |
|
| 775 |
+type AugmentedActiveDirectoryConfig struct {
|
|
| 776 |
+ // GroupQuery holds the template for an LDAP query that returns group entries |
|
| 777 |
+ GroupQuery LDAPQuery `json:"groupQuery" description:"the query for a group entry"` |
|
| 778 |
+ |
|
| 779 |
+ // GroupNameAttributes defines which attributes on an LDAP group entry will be interpreted as its' name |
|
| 780 |
+ GroupNameAttributes []string `json:"groupName" description:"the group name attributes"` |
|
| 781 |
+ |
|
| 782 |
+ // UserQuery holds the template for an LDAP query that returns user entries |
|
| 783 |
+ UserQuery LDAPQuery `json:"userQuery" description:"the query for a user entry"` |
|
| 784 |
+ |
|
| 785 |
+ // UserNameAttributes defines which attributes on an LDAP user entry will be interpreted as its' name |
|
| 786 |
+ UserNameAttributes []string `json:"userName" description:"the user name attributes"` |
|
| 787 |
+ |
|
| 788 |
+ // GroupMembershipAttributes defines which attributes on an LDAP user entry will be interpreted |
|
| 789 |
+ // as the groups it is a member of |
|
| 790 |
+ GroupMembershipAttributes []string `json:"groupMembership" description:"the group membership attributes"` |
|
| 791 |
+} |
|
| 792 |
+ |
|
| 793 |
+type LDAPQuery struct {
|
|
| 794 |
+ // The DN of the branch of the directory where all searches should start from |
|
| 795 |
+ BaseDN string `json:"baseDN" description:"the base DN for the search"` |
|
| 796 |
+ |
|
| 797 |
+ // The (optional) scope of the search. Can be: |
|
| 798 |
+ // base: only the base object, |
|
| 799 |
+ // one: all object on the base level, |
|
| 800 |
+ // sub: the entire subtree |
|
| 801 |
+ // Defaults to the entire subtree if not set |
|
| 802 |
+ Scope string `json:"scope" description:"the scope of the search"` |
|
| 803 |
+ |
|
| 804 |
+ // The (optional) behavior of the search with regards to alisases. Can be: |
|
| 805 |
+ // never: never dereference aliases, |
|
| 806 |
+ // search: only dereference in searching, |
|
| 807 |
+ // base: only dereference in finding the base object, |
|
| 808 |
+ // always: always dereference |
|
| 809 |
+ // Defaults to always dereferencing if not set |
|
| 810 |
+ DerefAliases string `json:"derefAliases" description:"the alias dereferencing behavior"` |
|
| 811 |
+ |
|
| 812 |
+ // TimeLimit holds the limit of time in seconds that any request to the server can remain outstanding |
|
| 813 |
+ // before the wait for a response is given up. If this is 0, no client-side limit is imposed |
|
| 814 |
+ TimeLimit int `json:"timeout" description:"the time limit for the query"` |
|
| 815 |
+ |
|
| 816 |
+ // Filter is a valid LDAP search filter that retrieves all relevant entries from the LDAP server with the base DN |
|
| 817 |
+ Filter string `json:"filter" description:"a valid LDAP filter for the query"` |
|
| 818 |
+ |
|
| 819 |
+ // QueryAttribute is the attribute for a filter that, when conjoined with the filter, retrieves the |
|
| 820 |
+ // specific LDAP entry from the LDAP server. (e.g. "cn", when formatted with "aGroupName" and conjoined |
|
| 821 |
+ // with "objectClass=groupOfNames", becomes (&(objectClass=groupOfNames)(cn=aGroupName))") |
|
| 822 |
+ QueryAttribute string `json:"queryAttribute" description:"the attribute to query on"` |
|
| 823 |
+} |
| ... | ... |
@@ -1,21 +1,21 @@ |
| 1 | 1 |
package v1 |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 |
- "testing" |
|
| 4 |
+ "testing" |
|
| 5 | 5 |
|
| 6 |
- "github.com/ghodss/yaml" |
|
| 7 |
- "k8s.io/kubernetes/pkg/runtime" |
|
| 8 |
- "k8s.io/kubernetes/pkg/util" |
|
| 6 |
+ "github.com/ghodss/yaml" |
|
| 7 |
+ "k8s.io/kubernetes/pkg/runtime" |
|
| 8 |
+ "k8s.io/kubernetes/pkg/util" |
|
| 9 | 9 |
|
| 10 |
- internal "github.com/openshift/origin/pkg/cmd/server/api" |
|
| 10 |
+ internal "github.com/openshift/origin/pkg/cmd/server/api" |
|
| 11 | 11 |
) |
| 12 | 12 |
|
| 13 | 13 |
const ( |
| 14 |
- // This constant lists all possible options for the node config file in v1 |
|
| 15 |
- // Before modifying this constant, ensure any changes have corresponding issues filed for: |
|
| 16 |
- // - documentation: https://github.com/openshift/openshift-docs/ |
|
| 17 |
- // - install: https://github.com/openshift/openshift-ansible/ |
|
| 18 |
- expectedSerializedNodeConfig = `allowDisabledDocker: false |
|
| 14 |
+ // This constant lists all possible options for the node config file in v1 |
|
| 15 |
+ // Before modifying this constant, ensure any changes have corresponding issues filed for: |
|
| 16 |
+ // - documentation: https://github.com/openshift/openshift-docs/ |
|
| 17 |
+ // - install: https://github.com/openshift/openshift-ansible/ |
|
| 18 |
+ expectedSerializedNodeConfig = `allowDisabledDocker: false |
|
| 19 | 19 |
apiVersion: v1 |
| 20 | 20 |
dnsDomain: "" |
| 21 | 21 |
dnsIP: "" |
| ... | ... |
@@ -43,12 +43,12 @@ servingInfo: |
| 43 | 43 |
volumeDirectory: "" |
| 44 | 44 |
` |
| 45 | 45 |
|
| 46 |
- // This constant lists all possible options for the master config file in v1. |
|
| 47 |
- // It also includes the fields for all the identity provider types. |
|
| 48 |
- // Before modifying this constant, ensure any changes have corresponding issues filed for: |
|
| 49 |
- // - documentation: https://github.com/openshift/openshift-docs/ |
|
| 50 |
- // - install: https://github.com/openshift/openshift-ansible/ |
|
| 51 |
- expectedSerializedMasterConfig = `apiLevels: null |
|
| 46 |
+ // This constant lists all possible options for the master config file in v1. |
|
| 47 |
+ // It also includes the fields for all the identity provider types. |
|
| 48 |
+ // Before modifying this constant, ensure any changes have corresponding issues filed for: |
|
| 49 |
+ // - documentation: https://github.com/openshift/openshift-docs/ |
|
| 50 |
+ // - install: https://github.com/openshift/openshift-ansible/ |
|
| 51 |
+ expectedSerializedMasterConfig = `apiLevels: null |
|
| 52 | 52 |
apiVersion: v1 |
| 53 | 53 |
assetConfig: |
| 54 | 54 |
extensionDevelopment: false |
| ... | ... |
@@ -265,58 +265,58 @@ servingInfo: |
| 265 | 265 |
) |
| 266 | 266 |
|
| 267 | 267 |
func TestNodeConfig(t *testing.T) {
|
| 268 |
- config := &internal.NodeConfig{
|
|
| 269 |
- PodManifestConfig: &internal.PodManifestConfig{},
|
|
| 270 |
- } |
|
| 271 |
- serializedConfig, err := writeYAML(config) |
|
| 272 |
- if err != nil {
|
|
| 273 |
- t.Fatal(err) |
|
| 274 |
- } |
|
| 275 |
- if string(serializedConfig) != expectedSerializedNodeConfig {
|
|
| 276 |
- t.Errorf("Diff:\n-------------\n%s", util.StringDiff(string(serializedConfig), expectedSerializedNodeConfig))
|
|
| 277 |
- } |
|
| 268 |
+ config := &internal.NodeConfig{
|
|
| 269 |
+ PodManifestConfig: &internal.PodManifestConfig{},
|
|
| 270 |
+ } |
|
| 271 |
+ serializedConfig, err := writeYAML(config) |
|
| 272 |
+ if err != nil {
|
|
| 273 |
+ t.Fatal(err) |
|
| 274 |
+ } |
|
| 275 |
+ if string(serializedConfig) != expectedSerializedNodeConfig {
|
|
| 276 |
+ t.Errorf("Diff:\n-------------\n%s", util.StringDiff(string(serializedConfig), expectedSerializedNodeConfig))
|
|
| 277 |
+ } |
|
| 278 | 278 |
} |
| 279 | 279 |
|
| 280 | 280 |
func TestMasterConfig(t *testing.T) {
|
| 281 |
- config := &internal.MasterConfig{
|
|
| 282 |
- KubernetesMasterConfig: &internal.KubernetesMasterConfig{},
|
|
| 283 |
- EtcdConfig: &internal.EtcdConfig{},
|
|
| 284 |
- OAuthConfig: &internal.OAuthConfig{
|
|
| 285 |
- IdentityProviders: []internal.IdentityProvider{
|
|
| 286 |
- {Provider: runtime.EmbeddedObject{Object: &internal.BasicAuthPasswordIdentityProvider{}}},
|
|
| 287 |
- {Provider: runtime.EmbeddedObject{Object: &internal.AllowAllPasswordIdentityProvider{}}},
|
|
| 288 |
- {Provider: runtime.EmbeddedObject{Object: &internal.DenyAllPasswordIdentityProvider{}}},
|
|
| 289 |
- {Provider: runtime.EmbeddedObject{Object: &internal.HTPasswdPasswordIdentityProvider{}}},
|
|
| 290 |
- {Provider: runtime.EmbeddedObject{Object: &internal.LDAPPasswordIdentityProvider{}}},
|
|
| 291 |
- {Provider: runtime.EmbeddedObject{Object: &internal.RequestHeaderIdentityProvider{}}},
|
|
| 292 |
- {Provider: runtime.EmbeddedObject{Object: &internal.GitHubIdentityProvider{}}},
|
|
| 293 |
- {Provider: runtime.EmbeddedObject{Object: &internal.GoogleIdentityProvider{}}},
|
|
| 294 |
- {Provider: runtime.EmbeddedObject{Object: &internal.OpenIDIdentityProvider{}}},
|
|
| 295 |
- }, |
|
| 296 |
- SessionConfig: &internal.SessionConfig{},
|
|
| 297 |
- }, |
|
| 298 |
- AssetConfig: &internal.AssetConfig{},
|
|
| 299 |
- DNSConfig: &internal.DNSConfig{},
|
|
| 300 |
- } |
|
| 301 |
- serializedConfig, err := writeYAML(config) |
|
| 302 |
- if err != nil {
|
|
| 303 |
- t.Fatal(err) |
|
| 304 |
- } |
|
| 305 |
- if string(serializedConfig) != expectedSerializedMasterConfig {
|
|
| 306 |
- t.Errorf("Diff:\n-------------\n%s", util.StringDiff(string(serializedConfig), expectedSerializedMasterConfig))
|
|
| 307 |
- } |
|
| 281 |
+ config := &internal.MasterConfig{
|
|
| 282 |
+ KubernetesMasterConfig: &internal.KubernetesMasterConfig{},
|
|
| 283 |
+ EtcdConfig: &internal.EtcdConfig{},
|
|
| 284 |
+ OAuthConfig: &internal.OAuthConfig{
|
|
| 285 |
+ IdentityProviders: []internal.IdentityProvider{
|
|
| 286 |
+ {Provider: runtime.EmbeddedObject{Object: &internal.BasicAuthPasswordIdentityProvider{}}},
|
|
| 287 |
+ {Provider: runtime.EmbeddedObject{Object: &internal.AllowAllPasswordIdentityProvider{}}},
|
|
| 288 |
+ {Provider: runtime.EmbeddedObject{Object: &internal.DenyAllPasswordIdentityProvider{}}},
|
|
| 289 |
+ {Provider: runtime.EmbeddedObject{Object: &internal.HTPasswdPasswordIdentityProvider{}}},
|
|
| 290 |
+ {Provider: runtime.EmbeddedObject{Object: &internal.LDAPPasswordIdentityProvider{}}},
|
|
| 291 |
+ {Provider: runtime.EmbeddedObject{Object: &internal.RequestHeaderIdentityProvider{}}},
|
|
| 292 |
+ {Provider: runtime.EmbeddedObject{Object: &internal.GitHubIdentityProvider{}}},
|
|
| 293 |
+ {Provider: runtime.EmbeddedObject{Object: &internal.GoogleIdentityProvider{}}},
|
|
| 294 |
+ {Provider: runtime.EmbeddedObject{Object: &internal.OpenIDIdentityProvider{}}},
|
|
| 295 |
+ }, |
|
| 296 |
+ SessionConfig: &internal.SessionConfig{},
|
|
| 297 |
+ }, |
|
| 298 |
+ AssetConfig: &internal.AssetConfig{},
|
|
| 299 |
+ DNSConfig: &internal.DNSConfig{},
|
|
| 300 |
+ } |
|
| 301 |
+ serializedConfig, err := writeYAML(config) |
|
| 302 |
+ if err != nil {
|
|
| 303 |
+ t.Fatal(err) |
|
| 304 |
+ } |
|
| 305 |
+ if string(serializedConfig) != expectedSerializedMasterConfig {
|
|
| 306 |
+ t.Errorf("Diff:\n-------------\n%s", util.StringDiff(string(serializedConfig), expectedSerializedMasterConfig))
|
|
| 307 |
+ } |
|
| 308 | 308 |
|
| 309 | 309 |
} |
| 310 | 310 |
|
| 311 | 311 |
func writeYAML(obj runtime.Object) ([]byte, error) {
|
| 312 |
- json, err := Codec.Encode(obj) |
|
| 313 |
- if err != nil {
|
|
| 314 |
- return nil, err |
|
| 315 |
- } |
|
| 312 |
+ json, err := Codec.Encode(obj) |
|
| 313 |
+ if err != nil {
|
|
| 314 |
+ return nil, err |
|
| 315 |
+ } |
|
| 316 | 316 |
|
| 317 |
- content, err := yaml.JSONToYAML(json) |
|
| 318 |
- if err != nil {
|
|
| 319 |
- return nil, err |
|
| 320 |
- } |
|
| 321 |
- return content, err |
|
| 317 |
+ content, err := yaml.JSONToYAML(json) |
|
| 318 |
+ if err != nil {
|
|
| 319 |
+ return nil, err |
|
| 320 |
+ } |
|
| 321 |
+ return content, err |
|
| 322 | 322 |
} |
| ... | ... |
@@ -9,7 +9,6 @@ import ( |
| 9 | 9 |
"k8s.io/kubernetes/pkg/util/fielderrors" |
| 10 | 10 |
|
| 11 | 11 |
"github.com/openshift/origin/pkg/auth/authenticator/redirector" |
| 12 |
- "github.com/openshift/origin/pkg/auth/ldaputil" |
|
| 13 | 12 |
"github.com/openshift/origin/pkg/auth/server/login" |
| 14 | 13 |
"github.com/openshift/origin/pkg/cmd/server/api" |
| 15 | 14 |
"github.com/openshift/origin/pkg/cmd/server/api/latest" |
| ... | ... |
@@ -145,50 +144,18 @@ func ValidateIdentityProvider(identityProvider api.IdentityProvider) ValidationR |
| 145 | 145 |
} |
| 146 | 146 |
|
| 147 | 147 |
func ValidateLDAPIdentityProvider(provider *api.LDAPPasswordIdentityProvider) ValidationResults {
|
| 148 |
- validationResults := ValidationResults{}
|
|
| 149 |
- |
|
| 150 |
- if len(provider.URL) == 0 {
|
|
| 151 |
- validationResults.AddErrors(fielderrors.NewFieldRequired("provider.url"))
|
|
| 152 |
- return validationResults |
|
| 153 |
- } |
|
| 154 |
- |
|
| 155 |
- u, err := ldaputil.ParseURL(provider.URL) |
|
| 156 |
- if err != nil {
|
|
| 157 |
- validationResults.AddErrors(fielderrors.NewFieldInvalid("provider.url", provider.URL, err.Error()))
|
|
| 158 |
- return validationResults |
|
| 159 |
- } |
|
| 160 |
- |
|
| 161 |
- // Make sure bindDN and bindPassword are both set, or both unset |
|
| 162 |
- // Both unset means an anonymous bind is used for search (https://tools.ietf.org/html/rfc4513#section-5.1.1) |
|
| 163 |
- // Both set means the name/password simple bind is used for search (https://tools.ietf.org/html/rfc4513#section-5.1.3) |
|
| 164 |
- if (len(provider.BindDN) == 0) != (len(provider.BindPassword) == 0) {
|
|
| 165 |
- validationResults.AddErrors(fielderrors.NewFieldInvalid("provider.bindDN", provider.BindDN, "bindDN and bindPassword must both be specified, or both be empty"))
|
|
| 166 |
- validationResults.AddErrors(fielderrors.NewFieldInvalid("provider.bindPassword", "<masked>", "bindDN and bindPassword must both be specified, or both be empty"))
|
|
| 167 |
- } |
|
| 168 |
- |
|
| 169 |
- if provider.Insecure {
|
|
| 170 |
- if u.Scheme == ldaputil.SchemeLDAPS {
|
|
| 171 |
- validationResults.AddErrors(fielderrors.NewFieldInvalid("provider.url", provider.URL, fmt.Sprintf("Cannot use %s scheme with insecure=true", u.Scheme)))
|
|
| 172 |
- } |
|
| 173 |
- if len(provider.CA) > 0 {
|
|
| 174 |
- validationResults.AddErrors(fielderrors.NewFieldInvalid("provider.ca", provider.CA, "Cannot specify a ca with insecure=true"))
|
|
| 175 |
- } |
|
| 176 |
- } else {
|
|
| 177 |
- if len(provider.CA) > 0 {
|
|
| 178 |
- validationResults.AddErrors(ValidateFile(provider.CA, "provider.ca")...) |
|
| 179 |
- } |
|
| 180 |
- } |
|
| 148 |
+ validationResults := ValidateLDAPClientConfig("provider",
|
|
| 149 |
+ provider.URL, |
|
| 150 |
+ provider.BindDN, |
|
| 151 |
+ provider.BindPassword, |
|
| 152 |
+ provider.CA, |
|
| 153 |
+ provider.Insecure) |
|
| 181 | 154 |
|
| 182 | 155 |
// At least one attribute to use as the user id is required |
| 183 | 156 |
if len(provider.Attributes.ID) == 0 {
|
| 184 | 157 |
validationResults.AddErrors(fielderrors.NewFieldInvalid("provider.attributes.id", "[]", "at least one id attribute is required (LDAP standard identity attribute is 'dn')"))
|
| 185 | 158 |
} |
| 186 | 159 |
|
| 187 |
- // Warn if insecure |
|
| 188 |
- if provider.Insecure {
|
|
| 189 |
- validationResults.AddWarnings(fielderrors.NewFieldInvalid("provider.insecure", provider.Insecure, "validating passwords over an insecure connection could allow them to be intercepted"))
|
|
| 190 |
- } |
|
| 191 |
- |
|
| 192 | 160 |
return validationResults |
| 193 | 161 |
} |
| 194 | 162 |
|
| ... | ... |
@@ -7,12 +7,14 @@ import ( |
| 7 | 7 |
"os" |
| 8 | 8 |
"strings" |
| 9 | 9 |
|
| 10 |
+ "github.com/go-ldap/ldap" |
|
| 10 | 11 |
"github.com/spf13/pflag" |
| 11 | 12 |
|
| 12 | 13 |
kvalidation "k8s.io/kubernetes/pkg/api/validation" |
| 13 | 14 |
"k8s.io/kubernetes/pkg/util" |
| 14 | 15 |
"k8s.io/kubernetes/pkg/util/fielderrors" |
| 15 | 16 |
|
| 17 |
+ "github.com/openshift/origin/pkg/auth/ldaputil" |
|
| 16 | 18 |
"github.com/openshift/origin/pkg/cmd/server/api" |
| 17 | 19 |
cmdflags "github.com/openshift/origin/pkg/cmd/util/flags" |
| 18 | 20 |
) |
| ... | ... |
@@ -240,3 +242,191 @@ func ValidateExtendedArguments(config api.ExtendedArguments, flagFunc func(*pfla |
| 240 | 240 |
|
| 241 | 241 |
return allErrs |
| 242 | 242 |
} |
| 243 |
+ |
|
| 244 |
+func ValidateLDAPSyncConfig(config api.LDAPSyncConfig) ValidationResults {
|
|
| 245 |
+ validationResults := ValidateLDAPClientConfig("config",
|
|
| 246 |
+ config.Host, |
|
| 247 |
+ config.BindDN, |
|
| 248 |
+ config.BindPassword, |
|
| 249 |
+ config.CA, |
|
| 250 |
+ config.Insecure) |
|
| 251 |
+ |
|
| 252 |
+ var numConfigs int |
|
| 253 |
+ |
|
| 254 |
+ if config.RFC2307Config != nil {
|
|
| 255 |
+ configResults := ValidateRFC2307Config(config.RFC2307Config) |
|
| 256 |
+ validationResults.AddErrors(configResults.Errors...) |
|
| 257 |
+ validationResults.AddWarnings(configResults.Warnings...) |
|
| 258 |
+ numConfigs++ |
|
| 259 |
+ } |
|
| 260 |
+ if config.ActiveDirectoryConfig != nil {
|
|
| 261 |
+ configResults := ValidateActiveDirectoryConfig(config.ActiveDirectoryConfig) |
|
| 262 |
+ validationResults.AddErrors(configResults.Errors...) |
|
| 263 |
+ validationResults.AddWarnings(configResults.Warnings...) |
|
| 264 |
+ numConfigs++ |
|
| 265 |
+ } |
|
| 266 |
+ if config.AugmentedActiveDirectoryConfig != nil {
|
|
| 267 |
+ configResults := ValidateAugmentedActiveDirectoryConfig(config.AugmentedActiveDirectoryConfig) |
|
| 268 |
+ validationResults.AddErrors(configResults.Errors...) |
|
| 269 |
+ validationResults.AddWarnings(configResults.Warnings...) |
|
| 270 |
+ numConfigs++ |
|
| 271 |
+ } |
|
| 272 |
+ if numConfigs != 1 {
|
|
| 273 |
+ validationResults.AddErrors(fielderrors.NewFieldInvalid("", config.LDAPSchemaSpecificConfig,
|
|
| 274 |
+ "only one schema-specific config is allowed")) |
|
| 275 |
+ } |
|
| 276 |
+ |
|
| 277 |
+ return validationResults |
|
| 278 |
+} |
|
| 279 |
+ |
|
| 280 |
+func ValidateLDAPClientConfig(parent, url, bindDN, bindPassword, CA string, insecure bool) ValidationResults {
|
|
| 281 |
+ validationResults := ValidationResults{}
|
|
| 282 |
+ |
|
| 283 |
+ if len(url) == 0 {
|
|
| 284 |
+ validationResults.AddErrors(fielderrors.NewFieldRequired(parent + ".host")) |
|
| 285 |
+ return validationResults |
|
| 286 |
+ } |
|
| 287 |
+ |
|
| 288 |
+ u, err := ldaputil.ParseURL(url) |
|
| 289 |
+ if err != nil {
|
|
| 290 |
+ validationResults.AddErrors(fielderrors.NewFieldInvalid(parent+".URL", url, err.Error())) |
|
| 291 |
+ return validationResults |
|
| 292 |
+ } |
|
| 293 |
+ |
|
| 294 |
+ // Make sure bindDN and bindPassword are both set, or both unset |
|
| 295 |
+ // Both unset means an anonymous bind is used for search (https://tools.ietf.org/html/rfc4513#section-5.1.1) |
|
| 296 |
+ // Both set means the name/password simple bind is used for search (https://tools.ietf.org/html/rfc4513#section-5.1.3) |
|
| 297 |
+ if (len(bindDN) == 0) != (len(bindPassword) == 0) {
|
|
| 298 |
+ validationResults.AddErrors(fielderrors.NewFieldInvalid(parent+".bindDN", bindDN, |
|
| 299 |
+ "bindDN and bindPassword must both be specified, or both be empty")) |
|
| 300 |
+ validationResults.AddErrors(fielderrors.NewFieldInvalid(parent+".bindPassword", "<masked>", |
|
| 301 |
+ "bindDN and bindPassword must both be specified, or both be empty")) |
|
| 302 |
+ } |
|
| 303 |
+ |
|
| 304 |
+ if insecure {
|
|
| 305 |
+ if u.Scheme == ldaputil.SchemeLDAPS {
|
|
| 306 |
+ validationResults.AddErrors(fielderrors.NewFieldInvalid(parent+".url", url, |
|
| 307 |
+ fmt.Sprintf("Cannot use %s scheme with insecure=true", u.Scheme)))
|
|
| 308 |
+ } |
|
| 309 |
+ if len(CA) > 0 {
|
|
| 310 |
+ validationResults.AddErrors(fielderrors.NewFieldInvalid(parent+".ca", CA, |
|
| 311 |
+ "Cannot specify a ca with insecure=true")) |
|
| 312 |
+ } |
|
| 313 |
+ } else {
|
|
| 314 |
+ if len(CA) > 0 {
|
|
| 315 |
+ validationResults.AddErrors(ValidateFile(CA, parent+".ca")...) |
|
| 316 |
+ } |
|
| 317 |
+ } |
|
| 318 |
+ |
|
| 319 |
+ // Warn if insecure |
|
| 320 |
+ if insecure {
|
|
| 321 |
+ validationResults.AddWarnings(fielderrors.NewFieldInvalid(parent+".insecure", insecure, |
|
| 322 |
+ "validating passwords over an insecure connection could allow them to be intercepted")) |
|
| 323 |
+ } |
|
| 324 |
+ |
|
| 325 |
+ return validationResults |
|
| 326 |
+} |
|
| 327 |
+ |
|
| 328 |
+func ValidateRFC2307Config(config *api.RFC2307Config) ValidationResults {
|
|
| 329 |
+ validationResults := ValidationResults{}
|
|
| 330 |
+ |
|
| 331 |
+ groupQueryResults := ValidateLDAPQuery("groupQuery", config.GroupQuery)
|
|
| 332 |
+ validationResults.AddErrors(groupQueryResults.Errors...) |
|
| 333 |
+ validationResults.AddWarnings(groupQueryResults.Warnings...) |
|
| 334 |
+ |
|
| 335 |
+ if len(config.GroupNameAttributes) == 0 {
|
|
| 336 |
+ validationResults.AddErrors(fielderrors.NewFieldRequired("groupName"))
|
|
| 337 |
+ } |
|
| 338 |
+ |
|
| 339 |
+ if len(config.GroupMembershipAttributes) == 0 {
|
|
| 340 |
+ validationResults.AddErrors(fielderrors.NewFieldRequired("groupMembership"))
|
|
| 341 |
+ } |
|
| 342 |
+ |
|
| 343 |
+ userQueryResults := ValidateLDAPQuery("userQuery", config.UserQuery)
|
|
| 344 |
+ validationResults.AddErrors(userQueryResults.Errors...) |
|
| 345 |
+ validationResults.AddWarnings(userQueryResults.Warnings...) |
|
| 346 |
+ |
|
| 347 |
+ if len(config.UserNameAttributes) == 0 {
|
|
| 348 |
+ validationResults.AddErrors(fielderrors.NewFieldRequired("userName"))
|
|
| 349 |
+ } |
|
| 350 |
+ |
|
| 351 |
+ return validationResults |
|
| 352 |
+} |
|
| 353 |
+ |
|
| 354 |
+func ValidateActiveDirectoryConfig(config *api.ActiveDirectoryConfig) ValidationResults {
|
|
| 355 |
+ validationResults := ValidationResults{}
|
|
| 356 |
+ |
|
| 357 |
+ userQueryResults := ValidateLDAPQuery("usersQuery", config.UsersQuery)
|
|
| 358 |
+ validationResults.AddErrors(userQueryResults.Errors...) |
|
| 359 |
+ validationResults.AddWarnings(userQueryResults.Warnings...) |
|
| 360 |
+ |
|
| 361 |
+ if len(config.UserNameAttributes) == 0 {
|
|
| 362 |
+ validationResults.AddErrors(fielderrors.NewFieldRequired("userName"))
|
|
| 363 |
+ } |
|
| 364 |
+ |
|
| 365 |
+ if len(config.GroupMembershipAttributes) == 0 {
|
|
| 366 |
+ validationResults.AddErrors(fielderrors.NewFieldRequired("groupMembership"))
|
|
| 367 |
+ } |
|
| 368 |
+ |
|
| 369 |
+ return validationResults |
|
| 370 |
+} |
|
| 371 |
+ |
|
| 372 |
+func ValidateAugmentedActiveDirectoryConfig(config *api.AugmentedActiveDirectoryConfig) ValidationResults {
|
|
| 373 |
+ validationResults := ValidationResults{}
|
|
| 374 |
+ |
|
| 375 |
+ groupQueryResults := ValidateLDAPQuery("groupQuery", config.GroupQuery)
|
|
| 376 |
+ validationResults.AddErrors(groupQueryResults.Errors...) |
|
| 377 |
+ validationResults.AddWarnings(groupQueryResults.Warnings...) |
|
| 378 |
+ |
|
| 379 |
+ if len(config.GroupNameAttributes) == 0 {
|
|
| 380 |
+ validationResults.AddErrors(fielderrors.NewFieldRequired("groupName"))
|
|
| 381 |
+ } |
|
| 382 |
+ |
|
| 383 |
+ if len(config.GroupMembershipAttributes) == 0 {
|
|
| 384 |
+ validationResults.AddErrors(fielderrors.NewFieldRequired("groupMembership"))
|
|
| 385 |
+ } |
|
| 386 |
+ |
|
| 387 |
+ userQueryResults := ValidateLDAPQuery("usersQuery", config.UsersQuery)
|
|
| 388 |
+ validationResults.AddErrors(userQueryResults.Errors...) |
|
| 389 |
+ validationResults.AddWarnings(userQueryResults.Warnings...) |
|
| 390 |
+ |
|
| 391 |
+ if len(config.UserNameAttributes) == 0 {
|
|
| 392 |
+ validationResults.AddErrors(fielderrors.NewFieldRequired("userName"))
|
|
| 393 |
+ } |
|
| 394 |
+ return validationResults |
|
| 395 |
+} |
|
| 396 |
+ |
|
| 397 |
+func ValidateLDAPQuery(queryName string, query api.LDAPQuery) ValidationResults {
|
|
| 398 |
+ validationResults := ValidationResults{}
|
|
| 399 |
+ |
|
| 400 |
+ if _, err := ldap.ParseDN(query.BaseDN); err != nil {
|
|
| 401 |
+ validationResults.AddErrors(fielderrors.NewFieldInvalid(queryName+".baseDN", query.BaseDN, |
|
| 402 |
+ fmt.Sprintf("invalid base DN for search: %v", err)))
|
|
| 403 |
+ } |
|
| 404 |
+ |
|
| 405 |
+ if len(query.Scope) > 0 {
|
|
| 406 |
+ if _, err := ldaputil.DetermineLDAPScope(query.Scope); err != nil {
|
|
| 407 |
+ validationResults.AddErrors(fielderrors.NewFieldInvalid(queryName+".scope", query.Scope, |
|
| 408 |
+ "invalid LDAP search scope")) |
|
| 409 |
+ } |
|
| 410 |
+ } |
|
| 411 |
+ |
|
| 412 |
+ if len(query.DerefAliases) > 0 {
|
|
| 413 |
+ if _, err := ldaputil.DetermineDerefAliasesBehavior(query.DerefAliases); err != nil {
|
|
| 414 |
+ validationResults.AddErrors(fielderrors.NewFieldInvalid(queryName+".derefAliases", |
|
| 415 |
+ query.DerefAliases, "LDAP alias dereferencing instruction invalid")) |
|
| 416 |
+ } |
|
| 417 |
+ } |
|
| 418 |
+ |
|
| 419 |
+ if query.TimeLimit < 0 {
|
|
| 420 |
+ validationResults.AddErrors(fielderrors.NewFieldInvalid(queryName+".timeout", query.TimeLimit, |
|
| 421 |
+ "timeout must be equal to or greater than zero")) |
|
| 422 |
+ } |
|
| 423 |
+ |
|
| 424 |
+ if _, err := ldap.CompileFilter(query.Filter); err != nil {
|
|
| 425 |
+ validationResults.AddErrors(fielderrors.NewFieldInvalid(queryName+".filter", query.Filter, |
|
| 426 |
+ fmt.Sprintf("invalid query filter: %v", err)))
|
|
| 427 |
+ } |
|
| 428 |
+ |
|
| 429 |
+ return validationResults |
|
| 430 |
+} |
| ... | ... |
@@ -478,21 +478,18 @@ func (c *AuthConfig) getPasswordAuthenticator(identityProvider configapi.Identit |
| 478 | 478 |
return nil, fmt.Errorf("Error parsing LDAPPasswordIdentityProvider URL: %v", err)
|
| 479 | 479 |
} |
| 480 | 480 |
|
| 481 |
- tlsConfig := &tls.Config{}
|
|
| 482 |
- if len(provider.CA) > 0 {
|
|
| 483 |
- roots, err := util.CertPoolFromFile(provider.CA) |
|
| 484 |
- if err != nil {
|
|
| 485 |
- return nil, fmt.Errorf("error loading cert pool from ca file %s: %v", provider.CA, err)
|
|
| 486 |
- } |
|
| 487 |
- tlsConfig.RootCAs = roots |
|
| 481 |
+ clientConfig, err := ldaputil.NewLDAPClientConfig(provider.URL, |
|
| 482 |
+ provider.BindDN, |
|
| 483 |
+ provider.BindPassword, |
|
| 484 |
+ provider.CA, |
|
| 485 |
+ provider.Insecure) |
|
| 486 |
+ if err != nil {
|
|
| 487 |
+ return nil, err |
|
| 488 | 488 |
} |
| 489 | 489 |
|
| 490 | 490 |
opts := ldappassword.Options{
|
| 491 |
- URL: url, |
|
| 492 |
- ClientConfig: ldaputil.NewLDAPClientConfig(url, provider.Insecure, tlsConfig), |
|
| 493 |
- BindDN: provider.BindDN, |
|
| 494 |
- BindPassword: provider.BindPassword, |
|
| 495 |
- |
|
| 491 |
+ URL: url, |
|
| 492 |
+ ClientConfig: clientConfig, |
|
| 496 | 493 |
UserAttributeDefiner: ldaputil.NewLDAPUserAttributeDefiner(provider.Attributes), |
| 497 | 494 |
} |
| 498 | 495 |
return ldappassword.New(identityProvider.Name, opts, identityMapper) |
| 499 | 496 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,434 @@ |
| 0 |
+package authentication |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "fmt" |
|
| 4 |
+ "os" |
|
| 5 |
+ "reflect" |
|
| 6 |
+ "regexp" |
|
| 7 |
+ "strings" |
|
| 8 |
+ |
|
| 9 |
+ g "github.com/onsi/ginkgo" |
|
| 10 |
+ o "github.com/onsi/gomega" |
|
| 11 |
+ |
|
| 12 |
+ kapi "k8s.io/kubernetes/pkg/api" |
|
| 13 |
+ "k8s.io/kubernetes/pkg/fields" |
|
| 14 |
+ "k8s.io/kubernetes/pkg/labels" |
|
| 15 |
+ |
|
| 16 |
+ "github.com/openshift/origin/pkg/auth/ldaputil" |
|
| 17 |
+ "github.com/openshift/origin/pkg/client" |
|
| 18 |
+ "github.com/openshift/origin/pkg/cmd/experimental/syncgroups" |
|
| 19 |
+ configapi "github.com/openshift/origin/pkg/cmd/server/api" |
|
| 20 |
+ userapi "github.com/openshift/origin/pkg/user/api" |
|
| 21 |
+ exutil "github.com/openshift/origin/test/extended/util" |
|
| 22 |
+) |
|
| 23 |
+ |
|
| 24 |
+var _ = g.Describe("authentication: OpenLDAP build and deployment", func() {
|
|
| 25 |
+ defer g.GinkgoRecover() |
|
| 26 |
+ var ( |
|
| 27 |
+ imageStreamFixture = exutil.FixturePath("fixtures", "ldap", "ldapserver-imagestream.json")
|
|
| 28 |
+ imageStreamTargetFixture = exutil.FixturePath("fixtures", "ldap", "ldapserver-imagestream-testenv.json")
|
|
| 29 |
+ buildConfigFixture = exutil.FixturePath("fixtures", "ldap", "ldapserver-buildconfig.json")
|
|
| 30 |
+ deploymentConfigFixture = exutil.FixturePath("fixtures", "ldap", "ldapserver-deploymentconfig.json")
|
|
| 31 |
+ serviceConfigFixture = exutil.FixturePath("fixtures", "ldap", "ldapserver-service.json")
|
|
| 32 |
+ oc = exutil.NewCLI("openldap", exutil.KubeConfigPath())
|
|
| 33 |
+ ) |
|
| 34 |
+ |
|
| 35 |
+ g.Describe("Building and deploying an OpenLDAP server", func() {
|
|
| 36 |
+ g.It(fmt.Sprintf("should create a image from %s template and run it in a pod", buildConfigFixture), func() {
|
|
| 37 |
+ nameRegex := regexp.MustCompile(`"[A-Za-z0-9\-]+"`) |
|
| 38 |
+ oc.SetOutputDir(exutil.TestContext.OutputDir) |
|
| 39 |
+ |
|
| 40 |
+ g.By(fmt.Sprintf("calling oc create -f %s", imageStreamFixture))
|
|
| 41 |
+ imageStreamMessage, err := oc.Run("create").Args("-f", imageStreamFixture).Output()
|
|
| 42 |
+ o.Expect(err).NotTo(o.HaveOccurred()) |
|
| 43 |
+ |
|
| 44 |
+ imageStreamName := strings.Trim(nameRegex.FindString(imageStreamMessage), `"`) |
|
| 45 |
+ g.By("expecting the imagestream to fetch and tag the latest image")
|
|
| 46 |
+ err = exutil.WaitForAnImageStream(oc.REST().ImageStreams(oc.Namespace()), imageStreamName, |
|
| 47 |
+ exutil.CheckImageStreamLatestTagPopulatedFunc, exutil.CheckImageStreamTagNotFoundFunc) |
|
| 48 |
+ o.Expect(err).NotTo(o.HaveOccurred()) |
|
| 49 |
+ |
|
| 50 |
+ g.By(fmt.Sprintf("calling oc create -f %s", imageStreamTargetFixture))
|
|
| 51 |
+ err = oc.Run("create").Args("-f", imageStreamTargetFixture).Execute()
|
|
| 52 |
+ o.Expect(err).NotTo(o.HaveOccurred()) |
|
| 53 |
+ |
|
| 54 |
+ g.By(fmt.Sprintf("calling oc create -f %s", buildConfigFixture))
|
|
| 55 |
+ buildConfigMessage, err := oc.Run("create").Args("-f", buildConfigFixture).Output()
|
|
| 56 |
+ o.Expect(err).NotTo(o.HaveOccurred()) |
|
| 57 |
+ |
|
| 58 |
+ buildConfigName := strings.Trim(nameRegex.FindString(buildConfigMessage), `"`) |
|
| 59 |
+ g.By(fmt.Sprintf("calling oc start-build %s", buildConfigName))
|
|
| 60 |
+ buildName, err := oc.Run("start-build").Args(buildConfigName).Output()
|
|
| 61 |
+ o.Expect(err).NotTo(o.HaveOccurred()) |
|
| 62 |
+ |
|
| 63 |
+ g.By("expecting the build to be in Complete phase")
|
|
| 64 |
+ err = exutil.WaitForABuild(oc.REST().Builds(oc.Namespace()), buildName, |
|
| 65 |
+ exutil.CheckBuildSuccessFunc, exutil.CheckBuildFailedFunc) |
|
| 66 |
+ o.Expect(err).NotTo(o.HaveOccurred()) |
|
| 67 |
+ |
|
| 68 |
+ g.By(fmt.Sprintf("calling oc create -f %s", deploymentConfigFixture))
|
|
| 69 |
+ deploymentConfigMessage, err := oc.Run("create").Args("-f", deploymentConfigFixture).Output()
|
|
| 70 |
+ o.Expect(err).NotTo(o.HaveOccurred()) |
|
| 71 |
+ |
|
| 72 |
+ deploymentConfigName := strings.Trim(nameRegex.FindString(deploymentConfigMessage), `"`) |
|
| 73 |
+ g.By(fmt.Sprintf("calling oc deploy %s", deploymentConfigName))
|
|
| 74 |
+ err = oc.Run("deploy").Args(deploymentConfigName).Execute()
|
|
| 75 |
+ o.Expect(err).NotTo(o.HaveOccurred()) |
|
| 76 |
+ |
|
| 77 |
+ g.By("expecting the deployment to be in Complete phase")
|
|
| 78 |
+ err = exutil.WaitForADeployment(oc.KubeREST().ReplicationControllers(oc.Namespace()), deploymentConfigName, |
|
| 79 |
+ exutil.CheckDeploymentCompletedFunc, exutil.CheckDeploymentFailedFunc) |
|
| 80 |
+ o.Expect(err).NotTo(o.HaveOccurred()) |
|
| 81 |
+ |
|
| 82 |
+ g.By(fmt.Sprintf("calling oc create -f %s", serviceConfigFixture))
|
|
| 83 |
+ err = oc.Run("create").Args("-f", serviceConfigFixture).Execute()
|
|
| 84 |
+ o.Expect(err).NotTo(o.HaveOccurred()) |
|
| 85 |
+ |
|
| 86 |
+ client := oc.KubeREST().Services(oc.Namespace()) |
|
| 87 |
+ ldapService, err := client.Get("openldap-server")
|
|
| 88 |
+ |
|
| 89 |
+ var testCases = []struct {
|
|
| 90 |
+ name string |
|
| 91 |
+ options syncgroups.SyncGroupsOptions |
|
| 92 |
+ expected []string |
|
| 93 |
+ seedGroups []userapi.Group //allows for groups to exist prior to the sync |
|
| 94 |
+ preSync bool //determines whether a sync should be performed before the sync to be tested |
|
| 95 |
+ }{
|
|
| 96 |
+ {
|
|
| 97 |
+ name: "schema 1 all ldap", |
|
| 98 |
+ options: syncgroups.SyncGroupsOptions{
|
|
| 99 |
+ Source: syncgroups.GroupSyncSourceLDAP, |
|
| 100 |
+ Scope: syncgroups.GroupSyncScopeAll, |
|
| 101 |
+ }, |
|
| 102 |
+ expected: []string{GroupName1, GroupName2, GroupName3},
|
|
| 103 |
+ seedGroups: []userapi.Group{},
|
|
| 104 |
+ preSync: false, |
|
| 105 |
+ }, |
|
| 106 |
+ {
|
|
| 107 |
+ name: "schema 1 whitelist LDAP", |
|
| 108 |
+ options: syncgroups.SyncGroupsOptions{
|
|
| 109 |
+ Source: syncgroups.GroupSyncSourceLDAP, |
|
| 110 |
+ Scope: syncgroups.GroupSyncScopeWhitelist, |
|
| 111 |
+ WhitelistContents: []string{GroupName1, GroupName2},
|
|
| 112 |
+ }, |
|
| 113 |
+ expected: []string{GroupName1, GroupName2},
|
|
| 114 |
+ seedGroups: []userapi.Group{},
|
|
| 115 |
+ preSync: false, |
|
| 116 |
+ }, |
|
| 117 |
+ {
|
|
| 118 |
+ name: "schema 1 all openshift no previous sync", |
|
| 119 |
+ options: syncgroups.SyncGroupsOptions{
|
|
| 120 |
+ Source: syncgroups.GroupSyncSourceOpenShift, |
|
| 121 |
+ Scope: syncgroups.GroupSyncScopeAll, |
|
| 122 |
+ }, |
|
| 123 |
+ expected: []string{}, // cant sync OpenShift groups that haven't been linked to an LDAP entry
|
|
| 124 |
+ seedGroups: []userapi.Group{},
|
|
| 125 |
+ preSync: false, |
|
| 126 |
+ }, |
|
| 127 |
+ {
|
|
| 128 |
+ name: "schema 1 all openshift with previous sync", |
|
| 129 |
+ options: syncgroups.SyncGroupsOptions{
|
|
| 130 |
+ Source: syncgroups.GroupSyncSourceOpenShift, |
|
| 131 |
+ Scope: syncgroups.GroupSyncScopeAll, |
|
| 132 |
+ }, |
|
| 133 |
+ expected: []string{GroupName1, GroupName2, GroupName3},
|
|
| 134 |
+ seedGroups: []userapi.Group{},
|
|
| 135 |
+ preSync: true, |
|
| 136 |
+ }, |
|
| 137 |
+ {
|
|
| 138 |
+ name: "schema 1 whitelist openshift no previous sync", |
|
| 139 |
+ options: syncgroups.SyncGroupsOptions{
|
|
| 140 |
+ Source: syncgroups.GroupSyncSourceOpenShift, |
|
| 141 |
+ Scope: syncgroups.GroupSyncScopeWhitelist, |
|
| 142 |
+ WhitelistContents: []string{GroupName1, GroupName2},
|
|
| 143 |
+ }, |
|
| 144 |
+ expected: []string{}, // cant sync OpenShift groups that haven't been linked to an LDAP entry
|
|
| 145 |
+ seedGroups: []userapi.Group{},
|
|
| 146 |
+ preSync: false, |
|
| 147 |
+ }, |
|
| 148 |
+ {
|
|
| 149 |
+ name: "schema 1 whitelist openshift with previous sync", |
|
| 150 |
+ options: syncgroups.SyncGroupsOptions{
|
|
| 151 |
+ Source: syncgroups.GroupSyncSourceOpenShift, |
|
| 152 |
+ Scope: syncgroups.GroupSyncScopeWhitelist, |
|
| 153 |
+ WhitelistContents: []string{GroupName1, GroupName2},
|
|
| 154 |
+ }, |
|
| 155 |
+ expected: []string{GroupName1, GroupName2},
|
|
| 156 |
+ seedGroups: []userapi.Group{},
|
|
| 157 |
+ preSync: true, |
|
| 158 |
+ }, |
|
| 159 |
+ // TODO: seed a group that shares name but has not been synced, check for Existing correctness |
|
| 160 |
+ } |
|
| 161 |
+ |
|
| 162 |
+ for _, testCase := range testCases {
|
|
| 163 |
+ g.By(fmt.Sprintf("Running test case: %s", testCase.name))
|
|
| 164 |
+ // determine LDAP server host:port |
|
| 165 |
+ host := ldapService.Spec.ClusterIP + ":389" |
|
| 166 |
+ |
|
| 167 |
+ // determine expected groups |
|
| 168 |
+ expectedGroups := makeGroups(host, testCase.expected) |
|
| 169 |
+ |
|
| 170 |
+ // populate config with test-case data |
|
| 171 |
+ testCase.options.Config = makeConfig(host) |
|
| 172 |
+ testCase.options.GroupInterface = oc.AdminREST().Groups() |
|
| 173 |
+ testCase.options.Stderr = os.Stderr |
|
| 174 |
+ testCase.options.Out = os.Stdout |
|
| 175 |
+ |
|
| 176 |
+ // Check that we are in the correct starting state |
|
| 177 |
+ g.By("Checking that the test case starts in the correct state")
|
|
| 178 |
+ groupList, err := oc.AdminREST().Groups().List(labels.Everything(), fields.Everything()) |
|
| 179 |
+ o.Expect(err).NotTo(o.HaveOccurred()) |
|
| 180 |
+ |
|
| 181 |
+ var stateErr error |
|
| 182 |
+ if len(groupList.Items) != 0 {
|
|
| 183 |
+ stateErr = fmt.Errorf("test %s beginning in incorrect state: should have no groups, had: %d, (%v)",
|
|
| 184 |
+ testCase.name, len(groupList.Items), groupList.Items) |
|
| 185 |
+ } |
|
| 186 |
+ o.Expect(stateErr).NotTo(o.HaveOccurred()) |
|
| 187 |
+ |
|
| 188 |
+ // Add groups if necessary |
|
| 189 |
+ g.By("Adding seed groups as necessary")
|
|
| 190 |
+ for _, groupToAdd := range testCase.seedGroups {
|
|
| 191 |
+ _, err = oc.AdminREST().Groups().Create(&groupToAdd) |
|
| 192 |
+ o.Expect(err).NotTo(o.HaveOccurred()) |
|
| 193 |
+ } |
|
| 194 |
+ |
|
| 195 |
+ // Preform "pre-sync" if required - this allows for OpenShift - sourced sync jobs to work |
|
| 196 |
+ // the OpenShift - sourced GroupListers look for the LDAPURLAnnotation annotation as well as the LDAPUIDAnnotation annotation |
|
| 197 |
+ g.By("Performing the pre-sync")
|
|
| 198 |
+ if testCase.preSync {
|
|
| 199 |
+ for _, group := range expectedGroups {
|
|
| 200 |
+ bareGroup := createBareGroup(group) |
|
| 201 |
+ _, err = oc.AdminREST().Groups().Create(&bareGroup) |
|
| 202 |
+ o.Expect(err).NotTo(o.HaveOccurred()) |
|
| 203 |
+ } |
|
| 204 |
+ } |
|
| 205 |
+ |
|
| 206 |
+ // Perform sync job |
|
| 207 |
+ g.By("Performing the sync job")
|
|
| 208 |
+ errs := testCase.options.Run() |
|
| 209 |
+ o.Expect(errs).NotTo(o.HaveOccurred()) |
|
| 210 |
+ |
|
| 211 |
+ // Check that the results are what we expected |
|
| 212 |
+ g.By("Validating results")
|
|
| 213 |
+ newGroupList, err := oc.AdminREST().Groups().List(labels.Everything(), fields.Everything()) |
|
| 214 |
+ o.Expect(err).NotTo(o.HaveOccurred()) |
|
| 215 |
+ |
|
| 216 |
+ ok, err := checkSetEquality(newGroupList.Items, expectedGroups) |
|
| 217 |
+ if err != nil || !ok {
|
|
| 218 |
+ stateErr = fmt.Errorf("group sync ended in incorrect state after test %s: %v", testCase.name, err)
|
|
| 219 |
+ } |
|
| 220 |
+ o.Expect(stateErr).NotTo(o.HaveOccurred()) |
|
| 221 |
+ |
|
| 222 |
+ // Clean up OpenShift etcd Group records |
|
| 223 |
+ cleanup(oc.AdminREST()) |
|
| 224 |
+ } |
|
| 225 |
+ }) |
|
| 226 |
+ }) |
|
| 227 |
+}) |
|
| 228 |
+ |
|
| 229 |
+const ( |
|
| 230 |
+ LDAPScopeWholeSubtree string = "sub" |
|
| 231 |
+ LDAPNeverDerefAliases string = "never" |
|
| 232 |
+ LDAPQueryTimeout int = 10 |
|
| 233 |
+ |
|
| 234 |
+ BaseDN string = "dc=example,dc=com" |
|
| 235 |
+ GroupBaseDN string = "ou=groups," + BaseDN |
|
| 236 |
+ UserBaseDN string = "ou=people," + BaseDN |
|
| 237 |
+ |
|
| 238 |
+ GroupFilter string = "objectClass=groupOfNames" |
|
| 239 |
+ GroupQueryAttribute string = "cn" |
|
| 240 |
+ UserFilter string = "objectClass=inetOrgPerson" |
|
| 241 |
+ UserQueryAttribute string = "cn" |
|
| 242 |
+ |
|
| 243 |
+ GroupMembershipAttribute string = "member" |
|
| 244 |
+ |
|
| 245 |
+ GroupNameAttribute1 string = "missing" |
|
| 246 |
+ GroupNameAttribute2 string = "cn" |
|
| 247 |
+ |
|
| 248 |
+ UserNameAttribute1 string = "missing" |
|
| 249 |
+ UserNameAttribute2 string = "name" |
|
| 250 |
+ UserNameAttribute3 string = "cn" |
|
| 251 |
+ |
|
| 252 |
+ GroupName1 string = "group1" |
|
| 253 |
+ GroupName2 string = "group2" |
|
| 254 |
+ GroupName3 string = "group3" |
|
| 255 |
+ |
|
| 256 |
+ UserName1 string = "Person1" |
|
| 257 |
+ UserName2 string = "Person2" |
|
| 258 |
+ UserName3 string = "Person3" |
|
| 259 |
+ UserName4 string = "Person4" |
|
| 260 |
+ UserName5 string = "Person5" |
|
| 261 |
+) |
|
| 262 |
+ |
|
| 263 |
+// makeGroups injects the run-dependent host into the expected group records and returns those |
|
| 264 |
+// specified by the which string array |
|
| 265 |
+func makeGroups(host string, which []string) []userapi.Group {
|
|
| 266 |
+ GroupRecord1 := userapi.Group{
|
|
| 267 |
+ ObjectMeta: kapi.ObjectMeta{
|
|
| 268 |
+ Name: GroupName1, |
|
| 269 |
+ Namespace: "", |
|
| 270 |
+ Annotations: map[string]string{
|
|
| 271 |
+ ldaputil.LDAPURLAnnotation: host, |
|
| 272 |
+ ldaputil.LDAPUIDAnnotation: GroupName1, |
|
| 273 |
+ }, |
|
| 274 |
+ }, |
|
| 275 |
+ Users: []string{
|
|
| 276 |
+ UserName1, |
|
| 277 |
+ UserName2, |
|
| 278 |
+ UserName3, |
|
| 279 |
+ UserName4, |
|
| 280 |
+ UserName5, |
|
| 281 |
+ }, |
|
| 282 |
+ } |
|
| 283 |
+ |
|
| 284 |
+ GroupRecord2 := userapi.Group{
|
|
| 285 |
+ ObjectMeta: kapi.ObjectMeta{
|
|
| 286 |
+ Name: GroupName2, |
|
| 287 |
+ Namespace: "", |
|
| 288 |
+ Annotations: map[string]string{
|
|
| 289 |
+ ldaputil.LDAPURLAnnotation: host, |
|
| 290 |
+ ldaputil.LDAPUIDAnnotation: GroupName2, |
|
| 291 |
+ }, |
|
| 292 |
+ }, |
|
| 293 |
+ Users: []string{
|
|
| 294 |
+ UserName1, |
|
| 295 |
+ UserName2, |
|
| 296 |
+ UserName3, |
|
| 297 |
+ }, |
|
| 298 |
+ } |
|
| 299 |
+ |
|
| 300 |
+ GroupRecord3 := userapi.Group{
|
|
| 301 |
+ ObjectMeta: kapi.ObjectMeta{
|
|
| 302 |
+ Name: GroupName3, |
|
| 303 |
+ Namespace: "", |
|
| 304 |
+ Annotations: map[string]string{
|
|
| 305 |
+ ldaputil.LDAPURLAnnotation: host, |
|
| 306 |
+ ldaputil.LDAPUIDAnnotation: GroupName3, |
|
| 307 |
+ }, |
|
| 308 |
+ }, |
|
| 309 |
+ Users: []string{
|
|
| 310 |
+ UserName1, |
|
| 311 |
+ UserName5, |
|
| 312 |
+ }, |
|
| 313 |
+ } |
|
| 314 |
+ |
|
| 315 |
+ expectedGroups := []userapi.Group{}
|
|
| 316 |
+ for _, expectedGroup := range which {
|
|
| 317 |
+ switch expectedGroup {
|
|
| 318 |
+ case GroupName3: |
|
| 319 |
+ expectedGroups = append(expectedGroups, GroupRecord3) |
|
| 320 |
+ case GroupName2: |
|
| 321 |
+ expectedGroups = append(expectedGroups, GroupRecord2) |
|
| 322 |
+ case GroupName1: |
|
| 323 |
+ expectedGroups = append(expectedGroups, GroupRecord1) |
|
| 324 |
+ } |
|
| 325 |
+ } |
|
| 326 |
+ |
|
| 327 |
+ return expectedGroups |
|
| 328 |
+} |
|
| 329 |
+ |
|
| 330 |
+func makeConfig(host string) configapi.LDAPSyncConfig {
|
|
| 331 |
+ // hard-coded config until config-file parsing is hashed out |
|
| 332 |
+ return configapi.LDAPSyncConfig{
|
|
| 333 |
+ Host: "ldap://" + host + "/", |
|
| 334 |
+ BindDN: "", |
|
| 335 |
+ BindPassword: "", |
|
| 336 |
+ Insecure: true, |
|
| 337 |
+ CA: "", |
|
| 338 |
+ |
|
| 339 |
+ LDAPGroupUIDToOpenShiftGroupNameMapping: make(map[string]string), |
|
| 340 |
+ |
|
| 341 |
+ LDAPSchemaSpecificConfig: configapi.LDAPSchemaSpecificConfig{
|
|
| 342 |
+ RFC2307Config: &configapi.RFC2307Config{
|
|
| 343 |
+ GroupQuery: configapi.LDAPQuery{
|
|
| 344 |
+ BaseDN: GroupBaseDN, |
|
| 345 |
+ Scope: LDAPScopeWholeSubtree, |
|
| 346 |
+ DerefAliases: LDAPNeverDerefAliases, |
|
| 347 |
+ TimeLimit: LDAPQueryTimeout, |
|
| 348 |
+ Filter: GroupFilter, |
|
| 349 |
+ QueryAttribute: GroupQueryAttribute, |
|
| 350 |
+ }, |
|
| 351 |
+ GroupNameAttributes: []string{GroupNameAttribute1, GroupNameAttribute2},
|
|
| 352 |
+ GroupMembershipAttributes: []string{GroupMembershipAttribute},
|
|
| 353 |
+ UserQuery: configapi.LDAPQuery{
|
|
| 354 |
+ BaseDN: UserBaseDN, |
|
| 355 |
+ Scope: LDAPScopeWholeSubtree, |
|
| 356 |
+ DerefAliases: LDAPNeverDerefAliases, |
|
| 357 |
+ TimeLimit: LDAPQueryTimeout, |
|
| 358 |
+ Filter: UserFilter, |
|
| 359 |
+ QueryAttribute: UserQueryAttribute, |
|
| 360 |
+ }, |
|
| 361 |
+ UserNameAttributes: []string{UserNameAttribute1, UserNameAttribute2, UserNameAttribute3},
|
|
| 362 |
+ }, |
|
| 363 |
+ }, |
|
| 364 |
+ } |
|
| 365 |
+} |
|
| 366 |
+ |
|
| 367 |
+// createBareGroup will create a new Group with only the data necessary for it to be accepted as having been previously |
|
| 368 |
+// synced from LDAP to allow us to add it to etcd and simulate a previous sync job |
|
| 369 |
+func createBareGroup(in userapi.Group) userapi.Group {
|
|
| 370 |
+ return userapi.Group{
|
|
| 371 |
+ ObjectMeta: kapi.ObjectMeta{
|
|
| 372 |
+ Name: in.Name, |
|
| 373 |
+ Namespace: in.Namespace, |
|
| 374 |
+ Annotations: map[string]string{
|
|
| 375 |
+ ldaputil.LDAPUIDAnnotation: in.Annotations[ldaputil.LDAPUIDAnnotation], |
|
| 376 |
+ ldaputil.LDAPURLAnnotation: in.Annotations[ldaputil.LDAPURLAnnotation], |
|
| 377 |
+ }, |
|
| 378 |
+ }, |
|
| 379 |
+ } |
|
| 380 |
+} |
|
| 381 |
+ |
|
| 382 |
+// checkSetEquality treats the incoming slices as sets and returns true if the sets are equal |
|
| 383 |
+func checkSetEquality(have, want []userapi.Group) (bool, error) {
|
|
| 384 |
+ // remove sync timestamp because it is not predictable and will cause DeepEqual to fail |
|
| 385 |
+ for _, obj := range have {
|
|
| 386 |
+ _, ok := obj.Annotations[ldaputil.LDAPSyncTimeAnnotation] |
|
| 387 |
+ if !ok {
|
|
| 388 |
+ return false, fmt.Errorf("synced group expected to have a %s annotation, but didn't",
|
|
| 389 |
+ ldaputil.LDAPSyncTimeAnnotation) |
|
| 390 |
+ } |
|
| 391 |
+ delete(obj.Annotations, ldaputil.LDAPSyncTimeAnnotation) |
|
| 392 |
+ } |
|
| 393 |
+ |
|
| 394 |
+ if len(have) != len(want) {
|
|
| 395 |
+ return false, fmt.Errorf("expected %v groups, got %v: wanted\n\t%#v\ngot\n\t%#v", len(want), len(have), want, have)
|
|
| 396 |
+ } |
|
| 397 |
+ |
|
| 398 |
+ // if what we want and what we have are the same size and size 0, we're done |
|
| 399 |
+ if len(want) == 0 {
|
|
| 400 |
+ return true, nil |
|
| 401 |
+ } |
|
| 402 |
+ |
|
| 403 |
+ // check that all entries in have exist in want |
|
| 404 |
+ for _, haveObj := range have {
|
|
| 405 |
+ wantWhatWeHave := false |
|
| 406 |
+ for _, wantObj := range want {
|
|
| 407 |
+ if reflect.DeepEqual(haveObj, wantObj) {
|
|
| 408 |
+ wantWhatWeHave = true |
|
| 409 |
+ } |
|
| 410 |
+ } |
|
| 411 |
+ if !wantWhatWeHave {
|
|
| 412 |
+ return false, fmt.Errorf("did not expect group record from sync job: %v", haveObj)
|
|
| 413 |
+ } |
|
| 414 |
+ } |
|
| 415 |
+ return true, nil |
|
| 416 |
+} |
|
| 417 |
+ |
|
| 418 |
+// cleanup removes all Group records from the OpenShift cluster to ready it for the next test |
|
| 419 |
+func cleanup(client *client.Client) error {
|
|
| 420 |
+ groupList, err := client.Groups().List(labels.Everything(), fields.Everything()) |
|
| 421 |
+ if err != nil {
|
|
| 422 |
+ return err |
|
| 423 |
+ } |
|
| 424 |
+ |
|
| 425 |
+ for _, group := range groupList.Items {
|
|
| 426 |
+ err = client.Groups().Delete(group.Name) |
|
| 427 |
+ if err != nil {
|
|
| 428 |
+ return err |
|
| 429 |
+ } |
|
| 430 |
+ } |
|
| 431 |
+ |
|
| 432 |
+ return nil |
|
| 433 |
+} |