feat(graph): [OCISDEV-794] allow multiple objectClasses on group creation

Add GroupAdditionalObjectClasses config field (env vars
OCIS_LDAP_GROUP_ADDITIONAL_OBJECTCLASSES /
GRAPH_LDAP_GROUP_ADDITIONAL_OBJECTCLASSES) that appends extra
objectClasses when creating groups in LDAP, alongside the existing
primary GroupObjectClass. Applied to both groupToLDAPAttrValues and
CreateLDAPGroupByDN.

Signed-off-by: Julian Koberg <julian.koberg@kiteworks.com>
This commit is contained in:
Julian Koberg
2026-04-20 12:17:40 +02:00
parent 147d428068
commit b76032d099
7 changed files with 61 additions and 35 deletions

View File

@@ -0,0 +1,8 @@
Enhancement: Allow multiple objectClasses on group creation
Added support for configuring additional LDAP objectClasses when creating groups.
The new `OCIS_LDAP_GROUP_ADDITIONAL_OBJECTCLASSES` / `GRAPH_LDAP_GROUP_ADDITIONAL_OBJECTCLASSES`
environment variable accepts a list of extra objectClasses that are set alongside the
primary `GRAPH_LDAP_GROUP_OBJECTCLASS` when a new group is created in LDAP.
https://github.com/owncloud/ocis/pull/12229

View File

@@ -52,6 +52,5 @@ olcObjectClasses: ( ownCloudOid:1.2.2 NAME 'ownCloudUser'
MAY ( ocExternalIdentity $ ownCloudUserEnabled $ ownCloudUserType $ ocLastSignInTimestamp $ ownCloudMemberOf $ ownCloudGuestOf $ ownCloudRole) ) MAY ( ocExternalIdentity $ ownCloudUserEnabled $ ownCloudUserType $ ocLastSignInTimestamp $ ownCloudMemberOf $ ownCloudGuestOf $ ownCloudRole) )
olcObjectClasses: ( ownCloudOid:1.2.3 NAME 'ownCloudGroup' olcObjectClasses: ( ownCloudOid:1.2.3 NAME 'ownCloudGroup'
DESC 'ownCloud Group LDAP Schema' DESC 'ownCloud Group LDAP Schema'
SUP groupOfNames AUXILIARY
STRUCTURAL
MAY ( ownCloudMemberOf ) ) MAY ( ownCloudMemberOf ) )

View File

@@ -60,7 +60,6 @@ services:
command: [ "-c", "ocis init || true; exec ocis server" ] command: [ "-c", "ocis init || true; exec ocis server" ]
environment: environment:
# Keycloak IDP specific configuration # Keycloak IDP specific configuration
PROXY_AUTOPROVISION_ACCOUNTS: "true"
PROXY_ROLE_ASSIGNMENT_DRIVER: "oidc" PROXY_ROLE_ASSIGNMENT_DRIVER: "oidc"
OCIS_OIDC_ISSUER: https://${KEYCLOAK_DOMAIN:-keycloak.owncloud.test}/realms/${KEYCLOAK_REALM:-oCIS} OCIS_OIDC_ISSUER: https://${KEYCLOAK_DOMAIN:-keycloak.owncloud.test}/realms/${KEYCLOAK_REALM:-oCIS}
PROXY_OIDC_REWRITE_WELLKNOWN: "true" PROXY_OIDC_REWRITE_WELLKNOWN: "true"
@@ -75,6 +74,9 @@ services:
PROXY_USER_CS3_CLAIM: "username" PROXY_USER_CS3_CLAIM: "username"
# INSECURE: needed if oCIS / Traefik is using self generated certificates # INSECURE: needed if oCIS / Traefik is using self generated certificates
OCIS_INSECURE: "${INSECURE:-true}" OCIS_INSECURE: "${INSECURE:-true}"
OCIS_ADD_RUN_SERVICES: "auth-app"
PROXY_ENABLE_APP_AUTH: true
AUTH_APP_ENABLE_IMPERSONATION: true
OCIS_EXCLUDE_RUN_SERVICES: "idp,idm" OCIS_EXCLUDE_RUN_SERVICES: "idp,idm"
GRAPH_ASSIGN_DEFAULT_USER_ROLE: "false" GRAPH_ASSIGN_DEFAULT_USER_ROLE: "false"
GRAPH_USERNAME_MATCH: "none" GRAPH_USERNAME_MATCH: "none"
@@ -90,7 +92,8 @@ services:
OCIS_LDAP_BIND_PASSWORD: ${LDAP_ADMIN_PASSWORD:-admin} OCIS_LDAP_BIND_PASSWORD: ${LDAP_ADMIN_PASSWORD:-admin}
OCIS_LDAP_GROUP_BASE_DN: "ou=groups,dc=owncloud,dc=com" OCIS_LDAP_GROUP_BASE_DN: "ou=groups,dc=owncloud,dc=com"
GRAPH_LDAP_GROUP_CREATE_BASE_DN: "ou=groups-ec730a6c-1b63-4b45-b83b-9e2311afdf85,ou=groups,dc=owncloud,dc=com" GRAPH_LDAP_GROUP_CREATE_BASE_DN: "ou=groups-ec730a6c-1b63-4b45-b83b-9e2311afdf85,ou=groups,dc=owncloud,dc=com"
OCIS_LDAP_GROUP_OBJECTCLASS: "owncloudGroup" OCIS_LDAP_GROUP_OBJECTCLASS: "groupOfNames"
OCIS_LDAP_GROUP_ADDITIONAL_OBJECTCLASSES: "ownCloudGroup"
OCIS_LDAP_USER_BASE_DN: "ou=users,dc=owncloud,dc=com" OCIS_LDAP_USER_BASE_DN: "ou=users,dc=owncloud,dc=com"
OCIS_LDAP_USER_OBJECTCLASS: "inetOrgPerson" OCIS_LDAP_USER_OBJECTCLASS: "inetOrgPerson"
LDAP_LOGIN_ATTRIBUTES: "uid" LDAP_LOGIN_ATTRIBUTES: "uid"
@@ -121,8 +124,6 @@ services:
OCIS_MULTI_INSTANCE_GUEST_ROLE: "user-light" OCIS_MULTI_INSTANCE_GUEST_ROLE: "user-light"
OCIS_LDAP_CROSS_INSTANCE_REFERENCE_TEMPLATE: "{{.Username}}@{{.Instancename}}.owncloud.test" OCIS_LDAP_CROSS_INSTANCE_REFERENCE_TEMPLATE: "{{.Username}}@{{.Instancename}}.owncloud.test"
OCIS_LDAP_INSTANCE_URL_TEMPLATE: "https://{{.Instancename}}.owncloud.test" OCIS_LDAP_INSTANCE_URL_TEMPLATE: "https://{{.Instancename}}.owncloud.test"
# FIXME: sync groups properly to keycloak and remove the next line
PROXY_AUTOPROVISION_CLAIM_GROUPS: ""
# specific for deployment example # specific for deployment example
PROXY_ROLE_ASSIGNMENT_OIDC_CLAIM: ownCloudRole PROXY_ROLE_ASSIGNMENT_OIDC_CLAIM: ownCloudRole
volumes: volumes:
@@ -153,7 +154,6 @@ services:
command: ["-c", "ocis init || true; ocis server"] command: ["-c", "ocis init || true; ocis server"]
environment: environment:
# Keycloak IDP specific configuration # Keycloak IDP specific configuration
PROXY_AUTOPROVISION_ACCOUNTS: "true"
PROXY_ROLE_ASSIGNMENT_DRIVER: "oidc" PROXY_ROLE_ASSIGNMENT_DRIVER: "oidc"
OCIS_OIDC_ISSUER: https://${KEYCLOAK_DOMAIN:-keycloak.owncloud.test}/realms/${KEYCLOAK_REALM:-oCIS} OCIS_OIDC_ISSUER: https://${KEYCLOAK_DOMAIN:-keycloak.owncloud.test}/realms/${KEYCLOAK_REALM:-oCIS}
PROXY_OIDC_REWRITE_WELLKNOWN: "true" PROXY_OIDC_REWRITE_WELLKNOWN: "true"
@@ -222,8 +222,6 @@ services:
OCIS_MULTI_INSTANCE_GUEST_ROLE: "user-light" OCIS_MULTI_INSTANCE_GUEST_ROLE: "user-light"
OCIS_LDAP_CROSS_INSTANCE_REFERENCE_TEMPLATE: "{{.Username}}@{{.Instancename}}.owncloud.test" OCIS_LDAP_CROSS_INSTANCE_REFERENCE_TEMPLATE: "{{.Username}}@{{.Instancename}}.owncloud.test"
OCIS_LDAP_INSTANCE_URL_TEMPLATE: "https://{{.Instancename}}.owncloud.test" OCIS_LDAP_INSTANCE_URL_TEMPLATE: "https://{{.Instancename}}.owncloud.test"
# FIXME: sync groups properly to keycloak and remove the next line
PROXY_AUTOPROVISION_CLAIM_GROUPS: ""
PROXY_ROLE_ASSIGNMENT_OIDC_CLAIM: ownCloudRole PROXY_ROLE_ASSIGNMENT_OIDC_CLAIM: ownCloudRole
volumes: volumes:
- ./config/ocis/csp-ocm.yaml:/etc/ocis/csp-ocm.yaml - ./config/ocis/csp-ocm.yaml:/etc/ocis/csp-ocm.yaml

View File

@@ -85,6 +85,7 @@ type LDAP struct {
GroupSearchScope string `yaml:"group_search_scope" env:"OCIS_LDAP_GROUP_SCOPE;GRAPH_LDAP_GROUP_SEARCH_SCOPE" desc:"LDAP search scope to use when looking up groups. Supported scopes are 'base', 'one' and 'sub'." introductionVersion:"pre5.0"` GroupSearchScope string `yaml:"group_search_scope" env:"OCIS_LDAP_GROUP_SCOPE;GRAPH_LDAP_GROUP_SEARCH_SCOPE" desc:"LDAP search scope to use when looking up groups. Supported scopes are 'base', 'one' and 'sub'." introductionVersion:"pre5.0"`
GroupFilter string `yaml:"group_filter" env:"OCIS_LDAP_GROUP_FILTER;GRAPH_LDAP_GROUP_FILTER" desc:"LDAP filter to add to the default filters for group searches." introductionVersion:"pre5.0"` GroupFilter string `yaml:"group_filter" env:"OCIS_LDAP_GROUP_FILTER;GRAPH_LDAP_GROUP_FILTER" desc:"LDAP filter to add to the default filters for group searches." introductionVersion:"pre5.0"`
GroupObjectClass string `yaml:"group_objectclass" env:"OCIS_LDAP_GROUP_OBJECTCLASS;GRAPH_LDAP_GROUP_OBJECTCLASS" desc:"The object class to use for groups in the default group search filter ('groupOfNames')." introductionVersion:"pre5.0"` GroupObjectClass string `yaml:"group_objectclass" env:"OCIS_LDAP_GROUP_OBJECTCLASS;GRAPH_LDAP_GROUP_OBJECTCLASS" desc:"The object class to use for groups in the default group search filter ('groupOfNames')." introductionVersion:"pre5.0"`
GroupAdditionalObjectClasses []string `yaml:"group_additional_objectclasses" env:"OCIS_LDAP_GROUP_ADDITIONAL_OBJECTCLASSES;GRAPH_LDAP_GROUP_ADDITIONAL_OBJECTCLASSES" desc:"Additional object classes to set when creating new groups (e.g. 'posixGroup'). The value of 'GRAPH_LDAP_GROUP_OBJECTCLASS' is always included." introductionVersion:"Deledda"`
GroupNameAttribute string `yaml:"group_name_attribute" env:"OCIS_LDAP_GROUP_SCHEMA_GROUPNAME;GRAPH_LDAP_GROUP_NAME_ATTRIBUTE" desc:"LDAP Attribute to use for the name of groups." introductionVersion:"pre5.0"` GroupNameAttribute string `yaml:"group_name_attribute" env:"OCIS_LDAP_GROUP_SCHEMA_GROUPNAME;GRAPH_LDAP_GROUP_NAME_ATTRIBUTE" desc:"LDAP Attribute to use for the name of groups." introductionVersion:"pre5.0"`
GroupMemberAttribute string `yaml:"group_member_attribute" env:"OCIS_LDAP_GROUP_SCHEMA_MEMBER;GRAPH_LDAP_GROUP_MEMBER_ATTRIBUTE" desc:"LDAP Attribute that is used for group members." introductionVersion:"pre5.0"` GroupMemberAttribute string `yaml:"group_member_attribute" env:"OCIS_LDAP_GROUP_SCHEMA_MEMBER;GRAPH_LDAP_GROUP_MEMBER_ATTRIBUTE" desc:"LDAP Attribute that is used for group members." introductionVersion:"pre5.0"`
GroupIDAttribute string `yaml:"group_id_attribute" env:"OCIS_LDAP_GROUP_SCHEMA_ID;GRAPH_LDAP_GROUP_ID_ATTRIBUTE" desc:"LDAP Attribute to use as the unique id for groups. This should be a stable globally unique ID like a UUID." introductionVersion:"pre5.0"` GroupIDAttribute string `yaml:"group_id_attribute" env:"OCIS_LDAP_GROUP_SCHEMA_ID;GRAPH_LDAP_GROUP_ID_ATTRIBUTE" desc:"LDAP Attribute to use as the unique id for groups. This should be a stable globally unique ID like a UUID." introductionVersion:"pre5.0"`

View File

@@ -67,6 +67,7 @@ type LDAP struct {
groupCreateBaseDN string groupCreateBaseDN string
groupFilter string groupFilter string
groupObjectClass string groupObjectClass string
groupAdditionalObjectClasses []string
groupIDisOctetString bool groupIDisOctetString bool
groupScope int groupScope int
groupAttributeMap groupAttributeMap groupAttributeMap groupAttributeMap
@@ -202,6 +203,7 @@ func NewLDAPBackend(lc ldap.Client, config config.LDAP, logger *log.Logger, inst
groupCreateBaseDN: config.GroupCreateBaseDN, groupCreateBaseDN: config.GroupCreateBaseDN,
groupFilter: config.GroupFilter, groupFilter: config.GroupFilter,
groupObjectClass: config.GroupObjectClass, groupObjectClass: config.GroupObjectClass,
groupAdditionalObjectClasses: config.GroupAdditionalObjectClasses,
groupIDisOctetString: config.GroupIDIsOctetString, groupIDisOctetString: config.GroupIDIsOctetString,
groupScope: groupScope, groupScope: groupScope,
groupAttributeMap: gam, groupAttributeMap: gam,
@@ -1306,8 +1308,9 @@ func replaceDN(fullDN *ldap.DN, newDN string) (string, error) {
func (i *LDAP) CreateLDAPGroupByDN(dn string) error { func (i *LDAP) CreateLDAPGroupByDN(dn string) error {
ar := ldap.NewAddRequest(dn, nil) ar := ldap.NewAddRequest(dn, nil)
objectClasses := append([]string{i.groupObjectClass, "top"}, i.groupAdditionalObjectClasses...)
attrs := map[string][]string{ attrs := map[string][]string{
"objectClass": {i.groupObjectClass, "top"}, "objectClass": objectClasses,
"member": {""}, "member": {""},
} }

View File

@@ -459,6 +459,8 @@ func (i *LDAP) groupToLDAPAttrValues(group libregraph.Group) (map[string][]strin
i.groupAttributeMap.member: {""}, i.groupAttributeMap.member: {""},
} }
attrs["objectClass"] = append(attrs["objectClass"], i.groupAdditionalObjectClasses...)
if !i.useServerUUID { if !i.useServerUUID {
attrs["owncloudUUID"] = []string{uuid.Must(uuid.NewV4()).String()} attrs["owncloudUUID"] = []string{uuid.Must(uuid.NewV4()).String()}
attrs["objectClass"] = append(attrs["objectClass"], "owncloud") attrs["objectClass"] = append(attrs["objectClass"], "owncloud")

View File

@@ -457,18 +457,29 @@ func TestGroupToLDAPAttrValuesUsesConfiguredObjectClass(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
groupObjectClass string groupObjectClass string
groupAdditionalObjectClasses []string
expectedObjectClasses []string
}{ }{
{ {
name: "default groupOfNames", name: "default groupOfNames",
groupObjectClass: "groupOfNames", groupObjectClass: "groupOfNames",
expectedObjectClasses: []string{"groupOfNames", "top", "owncloud"},
}, },
{ {
name: "custom groupOfUniqueNames", name: "custom groupOfUniqueNames",
groupObjectClass: "groupOfUniqueNames", groupObjectClass: "groupOfUniqueNames",
expectedObjectClasses: []string{"groupOfUniqueNames", "top", "owncloud"},
}, },
{ {
name: "custom posixGroup", name: "custom posixGroup",
groupObjectClass: "posixGroup", groupObjectClass: "posixGroup",
expectedObjectClasses: []string{"posixGroup", "top", "owncloud"},
},
{
name: "additional objectClasses",
groupObjectClass: "groupOfNames",
groupAdditionalObjectClasses: []string{"posixGroup", "extensibleObject"},
expectedObjectClasses: []string{"groupOfNames", "top", "posixGroup", "extensibleObject", "owncloud"},
}, },
} }
@@ -477,6 +488,7 @@ func TestGroupToLDAPAttrValuesUsesConfiguredObjectClass(t *testing.T) {
// Setup config with custom groupObjectClass // Setup config with custom groupObjectClass
testConfig := lconfig testConfig := lconfig
testConfig.GroupObjectClass = tt.groupObjectClass testConfig.GroupObjectClass = tt.groupObjectClass
testConfig.GroupAdditionalObjectClasses = tt.groupAdditionalObjectClasses
lm := &mocks.Client{} lm := &mocks.Client{}
b, err := getMockedBackend(lm, testConfig, &logger) b, err := getMockedBackend(lm, testConfig, &logger)
@@ -500,14 +512,17 @@ func TestGroupToLDAPAttrValuesUsesConfiguredObjectClass(t *testing.T) {
t.Fatal("Expected objectClass attribute to be present") t.Fatal("Expected objectClass attribute to be present")
} }
// Check objectClass has exactly 3 elements if len(objectClasses) != len(tt.expectedObjectClasses) {
if len(objectClasses) != 3 { t.Errorf("Expected objectClass to have %d elements, got %d: %v", len(tt.expectedObjectClasses), len(objectClasses), objectClasses)
t.Errorf("Expected objectClass to have exactly 3 elements, got %d: %v", len(objectClasses), objectClasses)
} }
// Check first element is the configured groupObjectClass (exact match) for i, expected := range tt.expectedObjectClasses {
if objectClasses[0] != tt.groupObjectClass { if i >= len(objectClasses) {
t.Errorf("Expected first objectClass to be '%s', got '%s'", tt.groupObjectClass, objectClasses[0]) break
}
if objectClasses[i] != expected {
t.Errorf("Expected objectClass[%d] to be '%s', got '%s'", i, expected, objectClasses[i])
}
} }
}) })
} }