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

@@ -80,15 +80,16 @@ type LDAP struct {
DisableUserMechanism string `yaml:"disable_user_mechanism" env:"OCIS_LDAP_DISABLE_USER_MECHANISM;GRAPH_DISABLE_USER_MECHANISM" desc:"An option to control the behavior for disabling users. Supported options are 'none', 'attribute' and 'group'. If set to 'group', disabling a user via API will add the user to the configured group for disabled users, if set to 'attribute' this will be done in the ldap user entry, if set to 'none' the disable request is not processed. Default is 'attribute'." introductionVersion:"pre5.0"` DisableUserMechanism string `yaml:"disable_user_mechanism" env:"OCIS_LDAP_DISABLE_USER_MECHANISM;GRAPH_DISABLE_USER_MECHANISM" desc:"An option to control the behavior for disabling users. Supported options are 'none', 'attribute' and 'group'. If set to 'group', disabling a user via API will add the user to the configured group for disabled users, if set to 'attribute' this will be done in the ldap user entry, if set to 'none' the disable request is not processed. Default is 'attribute'." introductionVersion:"pre5.0"`
LdapDisabledUsersGroupDN string `yaml:"ldap_disabled_users_group_dn" env:"OCIS_LDAP_DISABLED_USERS_GROUP_DN;GRAPH_DISABLED_USERS_GROUP_DN" desc:"The distinguished name of the group to which added users will be classified as disabled when 'disable_user_mechanism' is set to 'group'." introductionVersion:"pre5.0"` LdapDisabledUsersGroupDN string `yaml:"ldap_disabled_users_group_dn" env:"OCIS_LDAP_DISABLED_USERS_GROUP_DN;GRAPH_DISABLED_USERS_GROUP_DN" desc:"The distinguished name of the group to which added users will be classified as disabled when 'disable_user_mechanism' is set to 'group'." introductionVersion:"pre5.0"`
GroupBaseDN string `yaml:"group_base_dn" env:"OCIS_LDAP_GROUP_BASE_DN;GRAPH_LDAP_GROUP_BASE_DN" desc:"Search base DN for looking up LDAP groups." introductionVersion:"pre5.0"` GroupBaseDN string `yaml:"group_base_dn" env:"OCIS_LDAP_GROUP_BASE_DN;GRAPH_LDAP_GROUP_BASE_DN" desc:"Search base DN for looking up LDAP groups." introductionVersion:"pre5.0"`
GroupCreateBaseDN string `yaml:"group_create_base_dn" env:"GRAPH_LDAP_GROUP_CREATE_BASE_DN" desc:"Parent DN under which new groups are created. This DN needs to be subordinate to the 'GRAPH_LDAP_GROUP_BASE_DN'. This setting is only relevant when 'GRAPH_LDAP_SERVER_WRITE_ENABLED' is 'true'. It defaults to the value of 'GRAPH_LDAP_GROUP_BASE_DN'. All groups outside of this subtree are treated as readonly groups and cannot be updated." introductionVersion:"pre5.0"` GroupCreateBaseDN string `yaml:"group_create_base_dn" env:"GRAPH_LDAP_GROUP_CREATE_BASE_DN" desc:"Parent DN under which new groups are created. This DN needs to be subordinate to the 'GRAPH_LDAP_GROUP_BASE_DN'. This setting is only relevant when 'GRAPH_LDAP_SERVER_WRITE_ENABLED' is 'true'. It defaults to the value of 'GRAPH_LDAP_GROUP_BASE_DN'. All groups outside of this subtree are treated as readonly groups and cannot be updated." 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"` 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"`
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"` 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"`
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"` 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"`
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"` 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"`
GroupIDIsOctetString bool `yaml:"group_id_is_octet_string" env:"OCIS_LDAP_GROUP_SCHEMA_ID_IS_OCTETSTRING;GRAPH_LDAP_GROUP_SCHEMA_ID_IS_OCTETSTRING" desc:"Set this to true if the defined 'ID' attribute for groups is of the 'OCTETSTRING' syntax. This is required when using the 'objectGUID' attribute of Active Directory for the group ID's." 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"`
GroupIDIsOctetString bool `yaml:"group_id_is_octet_string" env:"OCIS_LDAP_GROUP_SCHEMA_ID_IS_OCTETSTRING;GRAPH_LDAP_GROUP_SCHEMA_ID_IS_OCTETSTRING" desc:"Set this to true if the defined 'ID' attribute for groups is of the 'OCTETSTRING' syntax. This is required when using the 'objectGUID' attribute of Active Directory for the group ID's." introductionVersion:"pre5.0"`
EducationResourcesEnabled bool `yaml:"education_resources_enabled" env:"GRAPH_LDAP_EDUCATION_RESOURCES_ENABLED" desc:"Enable LDAP support for managing education related resources." introductionVersion:"pre5.0"` EducationResourcesEnabled bool `yaml:"education_resources_enabled" env:"GRAPH_LDAP_EDUCATION_RESOURCES_ENABLED" desc:"Enable LDAP support for managing education related resources." introductionVersion:"pre5.0"`
EducationConfig LDAPEducationConfig EducationConfig LDAPEducationConfig

View File

@@ -66,8 +66,9 @@ type LDAP struct {
groupBaseDN string groupBaseDN string
groupCreateBaseDN string groupCreateBaseDN string
groupFilter string groupFilter string
groupObjectClass string groupObjectClass string
groupIDisOctetString bool groupAdditionalObjectClasses []string
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

@@ -455,20 +455,31 @@ func TestUpdateGroupName(t *testing.T) {
func TestGroupToLDAPAttrValuesUsesConfiguredObjectClass(t *testing.T) { 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])
}
} }
}) })
} }