Skip to content

Commit

Permalink
new saml meeting mapping (#2722)
Browse files Browse the repository at this point in the history
remove old direct meeting mapping
---------

Co-authored-by: rrenkert <[email protected]>
  • Loading branch information
hjanott and rrenkert authored Nov 25, 2024
1 parent 1bdd315 commit deac9e1
Show file tree
Hide file tree
Showing 12 changed files with 1,091 additions and 172 deletions.
58 changes: 57 additions & 1 deletion docs/actions/user.save_saml_account.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
pronoun: string,
is_active: boolean,
is_physical_person: boolean,
member_number: string,
// Additional meeting related data can be given. See below explanation on meeting mappers.
}
```

Expand All @@ -31,7 +33,61 @@ Extras to do on creation:

As you can see there is no password for local login and the user can't change it.

- Add user to the meeting by adding him to the group given in the organization-wide field-mapping as `"meeting": { "external_id": "xyz", "external_group_id": "delegates"}` if a `meeting`-entry is given. If it fails for any reason, a log entry is written, but no exception thrown. Add the user always to the group, if it fails try to add him to the default group.
### Meeting Mappers
- The saml attribute mapping can have a list of 'meeting_mappers' that can be used to assign users meeting related data. (See example below.)
- A mapper can be given a 'name' for debugging purposes.
- The 'external_id' maps to the meeting and is required (logged as warning if meeting does not exist). Multiple mappers can map to the same meeting.
- If 'allow_update' is set to false, the mapper is only used if the user does not already exist. If it is not given it defaults to true.
- Mappers are only used if every condition in the list of 'conditions' resolves to true. For this the 'attribute' in the payload data needs to match the string or regex given in 'condition'. If no condition is given this defaults to true.
- The actual mappings are objects or lists of objects of attribute-default pairs (exception: number, which only has the option of an attribute).
- The attribute refers to the payloads data.
- A default value can be given in case the payloads attribute does not exist or contains no data. (Logged as debug)
- Groups and structure levels are given as a list of attribute-default pairs.
- On conflict of multiple mappers mappings on a same meetings field the last given mappers data for that field is used. Exception to this are groups and structure levels. Their data is combined.
- Values for groups and structure levels can additionally be given in comma separated lists composed as a single string.
- Values for groups are interpreted as their external ID and structure levels as their name within that meeting.
- If no group exists for a meeting and no default is given, the meetings default group is used. (Logged as warning)
- If a structure level does not exist, it is created.
- Vote weights need to be given as 6 digit decimal strings.

```
"meeting_mappers": [{
"name": "Mapper-Name",
"external_id": "M2025",
"allow_update": "false",
"conditions": [{
"attribute": "membernumber",
"condition": "1426\d{4,6}$"
}, {
"attribute": "function",
"condition": "board"
}],
"mappings": {
"groups": [{
"attribute": "membership",
"default": "admin, standard"
}],
"structure_levels": [{
"attribute": "ovname",
"default": "struct1, struct2"
}],
"number": {"attribute": "p_number"},
"comment": {
"attribute": "idp_comment",
"default": "Group set via SSO"
},
"vote_weight": {
"attribute": "vote",
"default":"1.000000"
},
"present": {
"attribute": "present_key",
"default":"True"
}
}
}]
```
If you are using Keycloak as your SAML-server, make sure to fill the attributes of all users. Then you also need to configure for each attribute in 'Clients' a mapping for your Openslides services 'Client Scopes'. Choose 'User Attribute' and assign the 'User Attribute' as in the step before and the 'SAML Attribut Name' as defined in Openslides 'meeting_mappers'.

## Return Value

Expand Down
3 changes: 2 additions & 1 deletion global/data/example-data.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@
"gender": "gender",
"pronoun": "pronoun",
"is_active": "is_active",
"is_physical_person": "is_person"
"is_physical_person": "is_person",
"member_number": "member_number"
}
}
},
Expand Down
3 changes: 2 additions & 1 deletion global/data/initial-data.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@
"gender": "gender",
"pronoun": "pronoun",
"is_active": "is_active",
"is_physical_person": "is_person"
"is_physical_person": "is_person",
"member_number": "member_number"
}
}
},
Expand Down
58 changes: 41 additions & 17 deletions openslides_backend/action/actions/meeting_user/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,28 +54,52 @@ def get_history_information(self) -> HistoryInformation | None:
information = {}
for instance in self.instances:
instance_information = []
if "group_ids" in instance:
if len(instance["group_ids"]) == 1:
instance_information.extend(
[
"Participant added to group {} in meeting {}",
fqid_from_collection_and_id(
"group", instance["group_ids"][0]
),
]
fqids_per_collection = {
collection_name: [
fqid_from_collection_and_id(
collection_name,
_id,
)
else:
instance_information.append(
"Participant added to multiple groups in meeting {}",
)
else:
instance_information.append(
"Participant added to meeting {}",
)
for _id in ids
]
for collection_name in ["group", "structure_level"]
if (ids := instance.get(f"{collection_name}_ids"))
}
instance_information.append(
self.compose_history_string(list(fqids_per_collection.items()))
)
for collection_name, fqids in fqids_per_collection.items():
instance_information.extend(fqids)
instance_information.append(
fqid_from_collection_and_id("meeting", instance["meeting_id"]),
)
information[fqid_from_collection_and_id("user", instance["user_id"])] = (
instance_information
)
return information

def compose_history_string(
self, fqids_per_collection: list[tuple[str, list[str]]]
) -> str:
"""
Composes a string of the shape:
Participant added to groups {}, {} and structure levels {} in meeting {}.
"""
middle_sentence_parts = [
" ".join(
[ # prefix and to collection name if it's not the first in list
("and " if collection_name != fqids_per_collection[0][0] else "")
+ collection_name.replace("_", " ") # replace for human readablity
+ ("s" if len(fqids) != 1 else ""), # plural s
", ".join(["{}" for _ in range(len(fqids))]),
]
)
for collection_name, fqids in fqids_per_collection
]
return " ".join(
[
"Participant added to",
*middle_sentence_parts,
("in " if fqids_per_collection else "") + "meeting {}.",
]
)
23 changes: 17 additions & 6 deletions openslides_backend/action/actions/organization/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,24 @@ class OrganizationUpdate(
field: {**optional_str_schema, "max_length": 256}
for field in allowed_user_fields
}
saml_props["meeting"] = {
"type": ["object", "null"],
"properties": {
field: {**optional_str_schema, "max_length": 256}
for field in ("external_id", "external_group_id")
saml_props["meeting_mappers"] = {
"type": ["array", "null"],
"items": {
"type": "object",
"properties": {
**{
field: {**optional_str_schema, "max_length": 256}
for field in ("external_id", "name", "allow_update")
},
"conditions": {
"type": ["array", "null"],
"max_length": 256,
}, # , "items": {"object"}
"mappings": {"type": ["object", "array"], "max_length": 256},
},
"required": ["external_id"],
"additionalProperties": False,
},
"additionalProperties": False,
}
schema = DefaultSchema(Organization()).get_update_schema(
optional_properties=group_A_fields + group_B_fields,
Expand Down
2 changes: 1 addition & 1 deletion openslides_backend/action/actions/speaker/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ def validate_fields(self, instance: dict[str, Any]) -> dict[str, Any]:
user = self.datastore.get(user_fqid, ["is_present_in_meeting_ids"])
if meeting_id not in user.get("is_present_in_meeting_ids", ()):
raise ActionException(
"Only present users can be on the lists of speakers."
"Only present users can be on the list of speakers."
)

if not meeting.get("list_of_speakers_allow_multiple_speakers"):
Expand Down
Loading

0 comments on commit deac9e1

Please sign in to comment.