Gruppen-Steuerung #2

Merged
fenris merged 6 commits from task-416 into main 2025-10-23 19:16:29 +02:00
6 changed files with 299 additions and 290 deletions
Showing only changes of commit bef69283f6 - Show all commits

View file

@ -4597,7 +4597,7 @@ declare namespace lib_plankton.auth.oidc {
type type_token = string;
/**
*/
type type_userinfo = {
export type type_userinfo = {
name: (null | string);
label: (null | string);
email: (null | string);

View file

@ -21,38 +21,6 @@ along with »zeitbild«. If not, see <http://www.gnu.org/licenses/>.
namespace _zeitbild.api
{
/**
*/
function get_group_name(
group_name_raw : string
)
: string
{
return lib_plankton.string.coin(
"auto-{{name_raw}}",
{
"name_raw": group_name_raw,
}
);
}
/**
*/
function get_group_label(
group_name_raw : string
)
: string
{
return lib_plankton.string.coin(
"{{name_raw}}",
{
"name_raw": group_name_raw,
}
);
}
/**
*/
export function register_session_oidc(
@ -103,132 +71,39 @@ namespace _zeitbild.api
"execution": async (stuff) => {
const data : {
token : string;
userinfo : {
name : (null | string);
email : (null | string);
groups : (null | Array<string>);
};
userinfo : lib_plankton.auth.oidc.type_userinfo;
redirect_uri_template : string;
} = await _zeitbild.auth.oidc_handle_authorization_callback(
(stuff.headers["Cookie"] ?? stuff.headers["cookie"] ?? null),
stuff.query_parameters
);
if (data.userinfo.name === null)
{
return Promise.reject(
new Error(
"IDP did not return user name"
)
);
}
else
{
// groups
const group_ids : Array<_zeitbild.type_group_id> = await Promise.all<_zeitbild.type_group_id>(
(data.userinfo.groups ?? [])
.map(
async (group_name_raw) => {
const group_name : string = get_group_name(group_name_raw);
const group_id_raw : (null | _zeitbild.type_group_id) = await (
_zeitbild.repository.group.identify(group_name)
.catch(() => Promise.resolve(null))
);
if (group_id_raw === null)
{
const group_id : _zeitbild.type_group_id = await _zeitbild.service.group.add(
{
"name": group_name,
"label": get_group_label(group_name_raw),
}
);
return group_id;
}
else
{
const group_id : _zeitbild.type_group_id = group_id_raw;
await _zeitbild.service.group.change(
group_id,
{
"name": group_name,
"label": get_group_label(group_name_raw),
}
);
return group_id;
}
const user = await _zeitbild.auth.oidc_adapt_user(data.userinfo);
const session_key : string = await lib_plankton.session.begin(
user.object.name,
{
"data": {
"oidc_token": data.token,
}
}
);
return Promise.resolve(
{
"status_code": 200,
"data": lib_plankton.string.coin(
"<html><head><meta http-equiv=\"refresh\" content=\"0; url={{url}}\" /></head><body></body></html>",
{
"url": lib_plankton.string.coin(
data.redirect_uri_template,
{
"session_key": session_key,
}
),
}
)
);
const user_id : _zeitbild.type_user_id = await (async () => {
const user_object : _zeitbild.type_user_object = {
"name": (data.userinfo.name as string),
"groups": group_ids,
"email_address": data.userinfo.email,
"dav_token": null,
};
const user_id_raw : (null | _zeitbild.type_user_id) = await (
_zeitbild.service.user.identify(data.userinfo.name as string)
.catch(() => Promise.resolve(null))
);
if (user_id_raw === null)
{
// provision
const user_id : _zeitbild.type_user_id = await _zeitbild.service.user.add(
user_object
);
lib_plankton.log.info(
"user_provisioned",
{
"id": user_id,
"name": user_object.name,
}
);
return user_id;
}
else
{
const user_id : _zeitbild.type_user_id = user_id_raw;
// update
await _zeitbild.service.user.change(
user_id,
user_object
);
lib_plankton.log.info(
"user_updated",
{
"id": user_id,
"name": user_object.name,
}
);
return user_id;
}
}) ();
const session_key : string = await lib_plankton.session.begin(
data.userinfo.name,
{
"data": {
"oidc_token": data.token,
}
}
);
return Promise.resolve(
{
"status_code": 200,
"data": lib_plankton.string.coin(
"<html><head><meta http-equiv=\"refresh\" content=\"0; url={{url}}\" /></head><body></body></html>",
{
"url": lib_plankton.string.coin(
data.redirect_uri_template,
{
"session_key": session_key,
}
),
}
),
}
);
}
),
}
);
},
}
);

View file

@ -188,11 +188,7 @@ namespace _zeitbild.auth
) : Promise<
{
token : string;
userinfo : {
name : (null | string);
email : (null | string);
groups : (null | Array<string>);
};
userinfo : lib_plankton.auth.oidc.type_userinfo;
redirect_uri_template : string;
}
>
@ -206,11 +202,7 @@ namespace _zeitbild.auth
const state : string = data["state"];
const result : {
token : string;
userinfo : {
name : (null | string);
email : (null | string);
groups : (null | Array<string>);
};
userinfo : lib_plankton.auth.oidc.type_userinfo;
} = await lib_plankton.auth.oidc.handle_authorization_callback(
_subject_oidc,
cookie,
@ -219,11 +211,7 @@ namespace _zeitbild.auth
return Promise.resolve<
{
token : string;
userinfo : {
name : (null | string);
email : (null | string);
groups : (null | Array<string>);
};
userinfo : lib_plankton.auth.oidc.type_userinfo;
redirect_uri_template : string;
}
>(
@ -236,4 +224,144 @@ namespace _zeitbild.auth
}
}
/**
* @todo switch for enabling/disabling auto provisioning
*/
export async function oidc_adapt_user(
userinfo : lib_plankton.auth.oidc.type_userinfo
)
: Promise<
{
id : _zeitbild.type_user_id;
object : _zeitbild.type_user_object;
}
>
{
if (userinfo.name === null)
{
return Promise.reject(new Error("IDP did not return user name"));
}
else
{
// groups
const group_ids : Array<_zeitbild.type_group_id> = await (async () => {
const derive_name : ((group_name_raw : string) => string) = (
(group_name_raw) => lib_plankton.string.coin(
"auto-{{name_raw}}",
{
"name_raw": group_name_raw,
}
)
);
const derive_label : ((group_name_raw : string) => string) = (
(group_name_raw) => lib_plankton.string.coin(
"{{name_raw}}",
{
"name_raw": group_name_raw,
}
)
);
return Promise.all<_zeitbild.type_group_id>(
(userinfo.groups ?? [])
.map(
async (group_name_raw) => {
const group_name : string = derive_name(group_name_raw);
const group_id_raw : (null | _zeitbild.type_group_id) = await (
_zeitbild.repository.group.identify(group_name)
.catch(() => Promise.resolve(null))
);
if (group_id_raw === null)
{
// create
const group_id : _zeitbild.type_group_id = await _zeitbild.service.group.add(
{
"name": group_name,
"label": derive_label(group_name_raw),
}
);
lib_plankton.log.info(
"zeitbild.oidc_adapt_user.auto_provisioned_group",
{
"id": group_id,
"name": group_name,
}
);
return group_id;
}
else
{
// update
const group_id : _zeitbild.type_group_id = group_id_raw;
await _zeitbild.service.group.change(
group_id,
{
"name": group_name,
"label": derive_label(group_name_raw),
}
);
return group_id;
}
}
)
);
}) ();
// user
const user : {
id : _zeitbild.type_user_id;
object : _zeitbild.type_user_object;
} = await (async () => {
const user_id_raw : (null | _zeitbild.type_user_id) = await (
_zeitbild.service.user.identify(userinfo.name as string)
.catch(() => Promise.resolve(null))
);
if (user_id_raw === null)
{
// provision
const user_object : _zeitbild.type_user_object = {
"name": (userinfo.name as string),
"groups": group_ids,
"email_address": userinfo.email,
"dav_token": null,
};
const user_id : _zeitbild.type_user_id = await _zeitbild.service.user.add(
user_object
);
lib_plankton.log.info(
"user_provisioned",
{
"id": user_id,
"name": user_object.name,
}
);
return {"id": user_id, "object": user_object};
}
else
{
// update
const user_id : _zeitbild.type_user_id = user_id_raw;
const user_object : _zeitbild.type_user_object = await _zeitbild.service.user.get(user_id);
user_object.name = (userinfo.name as string);
user_object.groups = group_ids;
user_object.email_address = userinfo.email;
await _zeitbild.service.user.change(
user_id,
user_object
);
lib_plankton.log.info(
"user_updated",
{
"id": user_id,
"name": user_object.name,
}
);
return {"id": user_id, "object": user_object};
}
}) ();
return user;
}
}
}

View file

@ -328,7 +328,6 @@ namespace _zeitbild.repository.calendar
}
/**
*/
export async function read(
@ -601,7 +600,6 @@ namespace _zeitbild.repository.calendar
/**
* @todo caching
*/
export async function overview(
user_id : (null | _zeitbild.type_user_id)
@ -612,122 +610,152 @@ namespace _zeitbild.repository.calendar
>
>
{
type type_data = {
hits_core : Array<
{
key : int;
preview : Record<string, any>;
}
>;
hits_access_attributed_group : Array<
{
key : int;
preview : Record<string, any>;
}
>;
hits_access_attributed_user : Array<
{
key : int;
preview : Record<string, any>;
}
>;
};
return lib_plankton.cache.get_complex<any, Array<type_overview_entry>>(
_zeitbild.cache_regular,
"calendar_overview",
{
"user_id": user_id,
},
null,
() => (
lib_plankton.file.read("sql/calendar_overview.sql")
.then(
(template) => _zeitbild.database.get_implementation().query_free_get(
60,
async () => lib_plankton.call.convey(
{
"hits_core": await get_core_store().search(
{
"template": template,
"arguments": {
"user_id": user_id,
}
"expression": "TRUE",
"arguments": {}
}
)
)
.then(
(rows) => Promise.resolve(
lib_plankton.call.convey(
rows,
[
(x : Array<Record<string, any>>) => x.map(
(row : Record<string, any>) => ({
"id": row["id"],
"name": lib_plankton.call.convey(
row["name"],
[
// JSON.parse,
(x : Array<string>) => x[0],
]
),
"hue": lib_plankton.call.convey(
row["hue"],
[
// JSON.parse,
(x : Array<int>) => x[0],
(x : int) => (x / hue_scaling),
]
),
/**
* @todo use _zeitbild.access_level_determine
*/
"access_level": _zeitbild.access_level_determine_raw(
lib_plankton.call.convey(
row["access_public"],
),
"hits_access_attributed_group": await get_access_attributed_group_chest().search(
(user_id === null)
?
{
"expression": "TRUE",
"arguments": {}
}
:
{
"expression": "(group_id IN (SELECT group_id FROM user_groups WHERE (user_id = $user_id)))",
"arguments": {"user_id": user_id}
}
),
"hits_access_attributed_user": await get_access_attributed_user_chest().search(
(user_id === null)
?
{
"expression": "TRUE",
"arguments": {}
}
:
{
"expression": "(user_id = $user_id)",
"arguments": {"user_id": user_id}
}
),
},
[
// transform
(data : type_data) => data.hits_core.map(
(hit_core) => {
const calendar_id : _zeitbild.type_calendar_id = hit_core.key;
return {
"id": calendar_id,
"name": hit_core.preview["name"],
"hue": (hit_core.preview["hue"] / hue_scaling),
"access_level": _zeitbild.access_level_determine_raw(
hit_core.preview["access_public"],
(
(user_id === null)
?
null
:
{
"default": decode_access_level(hit_core.preview["access_level_default"]),
"group": lib_plankton.call.convey(
data.hits_access_attributed_group,
[
// JSON.parse,
(x : Array<boolean>) => x[0],
(x : Array<{key : int; preview : Record<string, any>}>) => x.filter(
hit_access_attributed_group => (
(hit_access_attributed_group.preview.calendar_id === calendar_id)
)
),
(x : Array<{key : int; preview : Record<string, any>}>) => x.map(
hit_access_attributed_group => hit_access_attributed_group.preview.level
),
(x : Array<int>) => x.map(
decode_access_level
),
]
),
(
(user_id === null)
?
null
:
{
"default": lib_plankton.call.convey(
row["access_level_default"],
[
// JSON.parse,
(x : Array<int>) => x[0],
decode_access_level,
]
"user": lib_plankton.call.convey(
data.hits_access_attributed_user,
[
(x : Array<{key : int; preview : Record<string, any>}>) => x.filter(
hits_access_attributed_user => (
(hits_access_attributed_user.preview.calendar_id === calendar_id)
)
),
"group": lib_plankton.call.convey(
row["access_level_attributed_group"],
[
// JSON.parse,
(x : Array<(null | int)>) => x.filter(y => (y !== null)),
(x : Array<int>) => x.map(decode_access_level),
]
(x : Array<{key : int; preview : Record<string, any>}>) => x.map(
hits_access_attributed_user => hits_access_attributed_user.preview.level
),
"user": lib_plankton.call.convey(
row["access_level_attributed_user"],
[
// JSON.parse,
(x : Array<(null | int)>) => x.filter(y => (y !== null)),
(x : Array<int>) => x.map(decode_access_level),
(x : Array<_zeitbild.enum_access_level>) => ((x.length > 0) ? x[0] : null),
]
(x : Array<int>) => x.map(
decode_access_level
),
}
)
),
})
),
(x : Array<type_overview_entry>) => x.filter(
(row) => (
! _zeitbild.access_level_order(
row.access_level,
_zeitbild.enum_access_level.none
)
(x : Array<_zeitbild.enum_access_level>) => (x[0] ?? null),
]
),
}
)
),
(x : Array<type_overview_entry>) => lib_plankton.list.sorted<type_overview_entry>(
x,
{
"compare_element": lib_plankton.order.order_lexicographic_pair_wrapped<type_overview_entry, _zeitbild.enum_access_level, int>(
row => row.access_level,
row => row.id,
{
"order_first": (a, b) => _zeitbild.access_level_order(b, a),
"order_second": (a, b) => (a <= b)
}
),
}
),
]
};
}
),
// only keep visible calendars
(x : Array<type_overview_entry>) => x.filter(
(row) => (
! _zeitbild.access_level_order(
row.access_level,
_zeitbild.enum_access_level.none
)
)
)
)
),
// sort by access level and name
(x : Array<type_overview_entry>) => lib_plankton.list.sorted<type_overview_entry>(
x,
{
"compare_element": lib_plankton.order.order_lexicographic_pair_wrapped<type_overview_entry, _zeitbild.enum_access_level, int>(
row => row.access_level,
row => row.id,
{
"order_first": (a, b) => _zeitbild.access_level_order(b, a),
"order_second": (a, b) => (a <= b)
}
),
}
),
]
)
);
}
}

View file

@ -1,15 +0,0 @@
SELECT
x.id AS id,
JSON_AGG(x.name) AS name,
JSON_AGG(x.hue) AS hue,
JSON_AGG(x.access_public) AS access_public,
JSON_AGG(x.access_level_default) AS access_level_default,
JSON_AGG(y1.level) AS access_level_attributed_group,
JSON_AGG(y2.level) AS access_level_attributed_user
FROM
calendars AS x
LEFT OUTER JOIN calendar_access_attributed_group AS y1 ON ((x.id = y1.calendar_id) AND (y1.group_id IN (SELECT group_id FROM user_groups WHERE (user_id = $user_id))))
LEFT OUTER JOIN calendar_access_attributed_user AS y2 ON ((x.id = y2.calendar_id) AND (y2.user_id = $user_id))
GROUP BY
x.id
;

View file

@ -18,7 +18,7 @@ cmd_tsc := ${dir_tools}/typescript/node_modules/.bin/tsc
## rules
.PHONY: default
default: node_modules sql ${dir_build}/zeitbild node_modules
default: node_modules ${dir_build}/zeitbild node_modules
.PHONY: node_modules
node_modules:
@ -26,13 +26,6 @@ node_modules:
@ ${cmd_log} "node modules …"
@ ${cmd_cp} -r -u ${dir_lib}/node/node_modules/* ${dir_build}/node_modules/
.PHONY: sql
sql: \
$(wildcard ${dir_source}/repositories/sql/*)
@ ${cmd_log} "sql …"
@ ${cmd_mkdir} ${dir_build}/sql
@ ${cmd_cp} -r -u $^ ${dir_build}/sql/
${dir_temp}/conf.ts: \
${dir_source}/conf.ts.tpl \
${dir_source}/conf.schema.json