diff --git a/src/kubedash/functions/k8s.py b/src/kubedash/functions/k8s.py index 412be6c2..5700fa5a 100644 --- a/src/kubedash/functions/k8s.py +++ b/src/kubedash/functions/k8s.py @@ -2202,18 +2202,52 @@ def k8sRoleBindingGet(obeject_name, namespace): ERROR = "k8sRoleBindingGet: %s" % error ErrorHandler(logger, "error", ERROR) return None, "Unknow Error" - -def k8sRoleBindingCreate(user_role, namespace, username): + +def k8sRoleBindingGroupGet(group_name, username_role, user_token): + k8sClientConfigGet(username_role, user_token) + group_role_binding = list() + namespace_list, error = k8sNamespaceListGet("Admin", None) + for ns in namespace_list: + role_binding_list = k8sRoleBindingListGet(username_role, user_token, ns) + for role_binding in role_binding_list: + if group_name in role_binding["group"]: + role_binding["namespace"] = ns + group_role_binding.append(role_binding) + return group_role_binding + +def k8sRoleBindingCreate(user_role, namespace, username, group_name): k8sClientConfigGet("Admin", None) with k8s_client.ApiClient() as api_client: api_instance = k8s_client.RbacAuthorizationV1Api(api_client) pretty = 'true' field_manager = 'KubeDash' - if email_check(username): - user = username.split("@")[0] + + if username: + if email_check(username): + user = username.split("@")[0] + else: + user = username + + obeject_name = user + "---" + "kubedash" + "---" + user_role + body_subjects = [ + k8s_client.V1Subject( + api_group = "rbac.authorization.k8s.io", + kind = "User", + name = username, + namespace = namespace, + ) + ] else: - user = username - obeject_name = user + "---" + "kubedash" + "---" + user_role + obeject_name = group_name + "---" + "kubedash" + "---" + user_role + body_subjects = [ + k8s_client.V1Subject( + api_group = "rbac.authorization.k8s.io", + kind = "Group", + name = group_name, + namespace = namespace, + ) + ] + body = k8s_client.V1RoleBinding( api_version = "rbac.authorization.k8s.io/v1", kind = "RoleBinding", @@ -2226,14 +2260,7 @@ def k8sRoleBindingCreate(user_role, namespace, username): kind = "ClusterRole", name = "template-namespaced-resources---" + user_role, ), - subjects = [ - k8s_client.V1Subject( - api_group = "rbac.authorization.k8s.io", - kind = "User", - name = username, - namespace = namespace, - ) - ] + subjects = body_subjects ) try: api_response = api_instance.create_namespaced_role_binding( @@ -2248,16 +2275,22 @@ def k8sRoleBindingCreate(user_role, namespace, username): return False, None -def k8sRoleBindingAdd(user_role, username, user_namespaces, user_all_namespaces): - if email_check(username): - user = username.split("@")[0] +def k8sRoleBindingAdd(user_role, username, group_name, user_namespaces, user_all_namespaces): + if username: + if email_check(username): + user = username.split("@")[0] + else: + user = username + + obeject_name = user + "---" + "kubedash" + "---" + user_role else: - user = username - obeject_name = user + "---" + "kubedash" + "---" + user_role + obeject_name = group_name + "---" + "kubedash" + "---" + user_role + if user_all_namespaces: namespace_list, error = k8sNamespaceListGet("Admin", None) else: namespace_list = user_namespaces + for namespace in namespace_list: is_rolebinding_exists, error = k8sRoleBindingGet(obeject_name, namespace) if error: @@ -2267,7 +2300,7 @@ def k8sRoleBindingAdd(user_role, username, user_namespaces, user_all_namespaces) ErrorHandler(logger, "CannotConnect", "RoleBinding %s alredy exists in %s namespace" % (obeject_name, namespace)) logger.info("RoleBinding %s alredy exists" % obeject_name) # WARNING else: - k8sRoleBindingCreate(user_role, namespace, username) + k8sRoleBindingCreate(user_role, namespace, username, group_name) ############################################################## @@ -2362,18 +2395,46 @@ def k8sClusterRoleBindingGet(obeject_name): ERROR = "k8sClusterRoleBindingGet: %s" % error ErrorHandler(logger, "error", ERROR) return None, "Unknow Error" - -def k8sClusterRoleBindingCreate(user_cluster_role, username): + +def k8sClusterRoleBindingGroupGet(group_name, username_role, user_token): + k8sClientConfigGet(username_role, user_token) + cluster_role_binding_list = k8sClusterRoleBindingListGet(username_role, user_token) + group_cluster_role_binding = list() + for cluster_role_binding in cluster_role_binding_list: + if group_name in cluster_role_binding["group"]: + group_cluster_role_binding.append(cluster_role_binding) + return group_cluster_role_binding + +def k8sClusterRoleBindingCreate(user_cluster_role, username, group_name): k8sClientConfigGet("Admin", None) with k8s_client.ApiClient() as api_client: api_instance = k8s_client.RbacAuthorizationV1Api(api_client) pretty = 'true' field_manager = 'KubeDash' - if email_check(username): - user = username.split("@")[0] + if username: + if email_check(username): + user = username.split("@")[0] + else: + user = username + + obeject_name = user + "---" + "kubedash" + "---" + user_cluster_role + body_subjects = [ + k8s_client.V1Subject( + api_group = "rbac.authorization.k8s.io", + kind = "User", + name = username, + ) + ] else: - user = username - obeject_name = user + "---" + "kubedash" + "---" + user_cluster_role + obeject_name = group_name + "---" + "kubedash" + "---" + user_cluster_role + body_subjects = [ + k8s_client.V1Subject( + api_group = "rbac.authorization.k8s.io", + kind = "Group", + name = group_name, + ) + ] + body = k8s_client.V1ClusterRoleBinding( api_version = "rbac.authorization.k8s.io/v1", kind = "ClusterRoleBinding", @@ -2385,13 +2446,7 @@ def k8sClusterRoleBindingCreate(user_cluster_role, username): kind = "ClusterRole", name = "template-cluster-resources---" + user_cluster_role, ), - subjects = [ - k8s_client.V1Subject( - api_group = "rbac.authorization.k8s.io", - kind = "User", - name = username, - ) - ] + subjects = body_subjects ) try: pi_response = api_instance.create_cluster_role_binding( @@ -2404,12 +2459,17 @@ def k8sClusterRoleBindingCreate(user_cluster_role, username): else: logger.info("ClusterRoleBinding %s alredy exists" % obeject_name) # WARNING -def k8sClusterRoleBindingAdd(user_cluster_role, username): - if email_check(username): - user = username.split("@")[0] +def k8sClusterRoleBindingAdd(user_cluster_role, username, group_name): + if username: + if email_check(username): + user = username.split("@")[0] + else: + user = username + + obeject_name = user + "---" + "kubedash" + "---" + user_cluster_role else: - user = username - obeject_name = user + "---" + "kubedash" + "---" + user_cluster_role + obeject_name = group_name + "---" + "kubedash" + "---" + user_cluster_role + is_clusterrolebinding_exists, error = k8sClusterRoleBindingGet(obeject_name) if error: ErrorHandler(logger, error, "get ClusterRoleBinding %s" % obeject_name) @@ -2418,7 +2478,7 @@ def k8sClusterRoleBindingAdd(user_cluster_role, username): ErrorHandler(logger, "CannotConnect", "ClusterRoleBinding %s alredy exists" % obeject_name) logger.info("ClusterRoleBinding %s alredy exists" % obeject_name) # WARNING else: - k8sClusterRoleBindingCreate(user_cluster_role, username) + k8sClusterRoleBindingCreate(user_cluster_role, username, group_name) ############################################################## # Security diff --git a/src/kubedash/functions/routes.py b/src/kubedash/functions/routes.py index 0dfa5498..7b2c5e7f 100644 --- a/src/kubedash/functions/routes.py +++ b/src/kubedash/functions/routes.py @@ -9,7 +9,8 @@ from functions.helper_functions import get_logger, email_check from functions.sso import SSOSererGet, get_auth_server_info, SSOServerUpdate, SSOServerCreate from functions.user import User, UsersRoles, Role, UserUpdate, UserCreate, UserDelete, \ - SSOUserCreate, SSOTokenUpdate, SSOTokenGet, UserUpdatePassword, KubectlConfigStore, KubectlConfig + SSOUserCreate, SSOTokenUpdate, SSOTokenGet, SSOGroupCreateFromList, SSOGroupsList, SSOGroupsMemberList, \ + UserUpdatePassword, KubectlConfigStore, KubectlConfig from functions.k8s import * from functions.registry import * @@ -432,6 +433,7 @@ def callback(): logger.info("Answer from clinet: %s" % x.text) except: pass +## Kubectl config end email = user_data['email'] username = user_data["preferred_username"] @@ -440,9 +442,11 @@ def callback(): if user is None: SSOUserCreate(username, email, user_token, "OpenID") + SSOGroupCreateFromList(username, user_data["groups"]) user = User.query.filter_by(username=username, user_type = "OpenID").first() else: SSOTokenUpdate(username, user_token) + SSOGroupCreateFromList(username, user_data["groups"]) user_role = UsersRoles.query.filter_by(user_id=user.id).first() role = Role.query.filter_by(id=user_role.role_id).first() @@ -1635,19 +1639,19 @@ def users_privileges_edit(): user_namespaces_2 = request.form.getlist('user_namespaces_2') if user_cluster_role: - k8sClusterRoleBindingAdd(user_cluster_role, username) + k8sClusterRoleBindingAdd(user_cluster_role, username, None) if user_namespaced_role_1: if user_all_namespaces_1: - k8sRoleBindingAdd(user_namespaced_role_1, username, None, user_all_namespaces_1) + k8sRoleBindingAdd(user_namespaced_role_1, username, None, None, user_all_namespaces_1) else: - k8sRoleBindingAdd(user_namespaced_role_1, username, user_namespaces_1, user_all_namespaces_1) + k8sRoleBindingAdd(user_namespaced_role_1, username, None, user_namespaces_1, user_all_namespaces_1) if user_namespaced_role_2: if user_all_namespaces_2: - k8sRoleBindingAdd(user_namespaced_role_2, username, None, user_all_namespaces_2) + k8sRoleBindingAdd(user_namespaced_role_2, username, None, None, user_all_namespaces_2) else: - k8sRoleBindingAdd(user_namespaced_role_2, username, user_namespaces_2, user_all_namespaces_2) + k8sRoleBindingAdd(user_namespaced_role_2, username, None, user_namespaces_2, user_all_namespaces_2) if session['user_type'] == "OpenID": user_token = session['oauth_token'] @@ -1667,7 +1671,7 @@ def users_privileges_edit(): k8sClusterRolesAdd() return render_template( - 'user-privilege.html.j2', + 'user-privilege-edit.html.j2', username = username, user_role_template_list = user_role_template_list, namespace_list = namespace_list, @@ -1676,6 +1680,111 @@ def users_privileges_edit(): else: return redirect(url_for('routes.login')) +############################################################## +## Groups +############################################################## + +@routes.route("/groups", methods=['GET', 'POST']) +@login_required +def groups(): + selected = None + if session['user_type'] == "OpenID": + user_token = session['oauth_token'] + else: + user_token = None + + if request.method == 'POST': + selected = request.form.get('selected') + + groupe_list = SSOGroupsList() + + return render_template( + 'groups.html.j2', + selected = selected, + groupe_list = groupe_list, + ) + +@routes.route("/groups/privilege", methods=['GET', 'POST']) +@login_required +def groups_privilege(): + if session['user_type'] == "OpenID": + user_token = session['oauth_token'] + else: + user_token = None + + if request.method == 'POST': + group_name = request.form['group_name'] + + groupe_member_list = SSOGroupsMemberList(group_name) + + # TODO: privileges and members + group_cluster_role_binding = k8sClusterRoleBindingGroupGet(group_name, session['user_role'], user_token) + group_role_binding = k8sRoleBindingGroupGet(group_name, session['user_role'], user_token) + + return render_template( + 'group-privileges.html.j2', + group_name = group_name, + groupe_member_list = groupe_member_list, + group_role_binding = group_role_binding, + group_cluster_role_binding = group_cluster_role_binding, + ) + +@routes.route("/groups/privilege/edit", methods=['POST']) +@login_required +def groups_mapping(): + if request.method == 'POST': + group_name = request.form['group_name'] + + user_cluster_role = request.form.get('user_cluster_role') + user_namespaced_role_1 = request.form.get('user_namespaced_role_1') + user_all_namespaces_1 = request.form.get('user_all_namespaces_1') + user_namespaces_1 = request.form.getlist('user_namespaces_1') + user_namespaced_role_2 = request.form.get('user_namespaced_role_2') + user_all_namespaces_2 = request.form.get('user_all_namespaces_2') + user_namespaces_2 = request.form.getlist('user_namespaces_2') + + if user_cluster_role: + k8sClusterRoleBindingAdd(user_cluster_role, None, group_name) + + if user_namespaced_role_1: + if user_all_namespaces_1: + k8sRoleBindingAdd(user_namespaced_role_1, None, group_name, None, user_all_namespaces_1) + else: + k8sRoleBindingAdd(user_namespaced_role_1, None, group_name, user_namespaces_1, user_all_namespaces_1) + + if user_namespaced_role_2: + if user_all_namespaces_2: + k8sRoleBindingAdd(user_namespaced_role_2, None, group_name, None, user_all_namespaces_2) + else: + k8sRoleBindingAdd(user_namespaced_role_2, None, group_name, user_namespaces_2, user_all_namespaces_2) + + if session['user_type'] == "OpenID": + user_token = session['oauth_token'] + else: + user_token = None + + namespace_list, error = k8sNamespaceListGet(session['user_role'], user_token) + if not error: + user_role_template_list = k8sUserRoleTemplateListGet(session['user_role'], user_token) + user_clusterRole_template_list = k8sUserClusterRoleTemplateListGet(session['user_role'], user_token) + else: + user_role_template_list = [] + user_clusterRole_template_list = [] + + if not bool(user_clusterRole_template_list) or not bool(user_role_template_list): + from functions.k8s import k8sClusterRolesAdd + k8sClusterRolesAdd() + + return render_template( + 'group-privilege-edit.html.j2', + group_name = group_name, + user_role_template_list = user_role_template_list, + namespace_list = namespace_list, + user_clusterRole_template_list = user_clusterRole_template_list, + ) + else: + return redirect(url_for('routes.login')) + ############################################################## ## Service Account ############################################################## @@ -1777,6 +1886,7 @@ def role_bindings(): if request.method == 'POST': session['ns_select'] = request.form.get('ns_select') + rb_name = request.form.get('rb_name') namespace_list, error = k8sNamespaceListGet(session['user_role'], user_token) if not error: @@ -1788,6 +1898,7 @@ def role_bindings(): 'role-bindings.html.j2', role_bindings = role_bindings, namespaces = namespace_list, + rb_name = rb_name, ) ############################################################## @@ -1838,18 +1949,23 @@ def cluster_role_data(): ## Cluster Role Bindings ############################################################## -@routes.route("/cluster-role-bindings") +@routes.route("/cluster-role-bindings", methods=["GET", "POST"]) @login_required def cluster_role_bindings(): + crb_name = None if session['user_type'] == "OpenID": user_token = session['oauth_token'] else: user_token = None + if request.method == 'POST': + crb_name = request.form.get('crb_name') + cluster_role_bindings = k8sClusterRoleBindingListGet(session['user_role'], user_token) return render_template( 'cluster-role-bindings.html.j2', cluster_role_bindings = cluster_role_bindings, + crb_name = crb_name, ) ############################################################## diff --git a/src/kubedash/functions/user.py b/src/kubedash/functions/user.py index f88b4ad4..751d875b 100644 --- a/src/kubedash/functions/user.py +++ b/src/kubedash/functions/user.py @@ -5,6 +5,8 @@ from contextlib import nullcontext from flask_login import UserMixin from werkzeug.security import generate_password_hash +from datetime import datetime +from pytz import timezone ############################################################## ## functions @@ -23,12 +25,15 @@ class User(UserMixin, db.Model): username = db.Column(db.String(80), unique=True, nullable=False) password_hash = db.Column(db.String(300), nullable=True) email = db.Column(db.String(80), unique=True, nullable=True) - roles = db.relationship('Role', secondary='users_roles', - backref=db.backref('users', lazy='dynamic')) user_type = db.Column(db.String(5), nullable=False) tokens = db.Column(db.Text, nullable=True) + + roles = db.relationship('Role', secondary='users_roles', + backref=db.backref('users', lazy='dynamic')) kubectl_config = db.relationship('KubectlConfig', secondary='users_kubectl', backref=db.backref('users', lazy='dynamic')) + sso_groups = db.relationship('SSOGroups', secondary='sso_user_group_mapping', + backref=db.backref('users', lazy='dynamic')) def __repr__(self): return '' % self.username @@ -162,6 +167,9 @@ def UserUpdatePassword(username, password): else: return False +######################################################################## +# KubectlConfig +######################################################################## # Define the KubectlConfig data model class KubectlConfig(db.Model): __tablename__ = 'kubectl_config' @@ -188,4 +196,67 @@ def KubectlConfigStore(name, cluster, private_key_base64, user_certificate_base6 user_certificate = user_certificate_base64, ) db.session.add(kubectl_config) - db.session.commit() \ No newline at end of file + db.session.commit() + +######################################################################## +# SSO Groups +######################################################################## + +class SSOGroups(db.Model): + __tablename__ = 'sso_groups' + id = db.Column(db.Integer(), primary_key=True) + name = db.Column(db.String(50), nullable=False, server_default=u'', unique=True) + created = db.Column(db.DateTime, default=datetime.now().astimezone(timezone('Europe/Budapest')), nullable=False) + +class SSOUserGroups(db.Model): + __tablename__ = 'sso_user_group_mapping' + id = db.Column(db.Integer(), primary_key=True) + user_id = db.Column(db.Integer(), db.ForeignKey('users.id', ondelete='CASCADE')) + group_id = db.Column(db.Integer(), db.ForeignKey('sso_groups.id', ondelete='CASCADE')) + +def SSOGroupCreateFromList(username, groups): + for group in groups: + SSOGroupsCreate(username, group) + +def SSOGroupsCreate(username, groups_name): + user = User.query.filter_by(username=username).first() + sso_group_data = SSOGroups( + name = groups_name + ) + sso_groups = SSOGroups.query.filter_by(name=groups_name).first() + if not sso_groups: + db.session.add(sso_group_data) + db.session.commit() + user.sso_groups.append(sso_group_data) + else: + sso_user = SSOUserGroups.query.filter( + SSOUserGroups.group_id == sso_groups.id, + SSOUserGroups.user_id == user.id + ).first() + if not sso_user: + user.sso_groups.append(sso_group_data) + +def SSOGroupsList(): + sso_groups = SSOGroups.query.all() + sso_group_list = list() + for group in sso_groups: + group_data = { + "name": group.name, + "created": group.created, + } + sso_group_list.append(group_data) + return sso_group_list + +def SSOGroupsMemberList(sso_group): + sso_group_data = SSOGroups.query.filter(SSOGroups.name == sso_group).first() + sso_users = SSOUserGroups.query.filter(SSOUserGroups.group_id == sso_group_data.id).all() + user_list = list() + for sso_user in sso_users: + user = User.query.filter(User.id == sso_user.user_id).first() + user_data = { + "name": user.username, + "email": user.email, + "type": user.user_type, + } + user_list.append(user_data) + return user_list \ No newline at end of file diff --git a/src/kubedash/migrations/versions/f84ec038286c_sso_groups_sso_user_group_mapping_tables.py b/src/kubedash/migrations/versions/f84ec038286c_sso_groups_sso_user_group_mapping_tables.py new file mode 100644 index 00000000..a56d237d --- /dev/null +++ b/src/kubedash/migrations/versions/f84ec038286c_sso_groups_sso_user_group_mapping_tables.py @@ -0,0 +1,43 @@ +"""sso_groups, sso_user_group_mapping tables + +Revision ID: f84ec038286c +Revises: 39704d8c644a +Create Date: 2024-02-23 14:46:40.405792 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'f84ec038286c' +down_revision = '39704d8c644a' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('sso_groups', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=50), server_default='', nullable=False), + sa.Column('created', sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('name') + ) + op.create_table('sso_user_group_mapping', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.Column('group_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['group_id'], ['sso_groups.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('sso_user_group_mapping') + op.drop_table('sso_groups') + # ### end Alembic commands ### diff --git a/src/kubedash/templates/base.html.j2 b/src/kubedash/templates/base.html.j2 index 0381d12c..e6ff5b75 100644 --- a/src/kubedash/templates/base.html.j2 +++ b/src/kubedash/templates/base.html.j2 @@ -73,6 +73,23 @@ Interface + + + +