Skip to content

Commit

Permalink
Merge pull request #760 from mahadevan-karthi-dwp/feat-group-saml-links
Browse files Browse the repository at this point in the history
feat: support for group SAML links

- closes #549
  • Loading branch information
TimKnight-DWP committed May 23, 2024
2 parents 13c3511 + 5529a1f commit e2a44dc
Show file tree
Hide file tree
Showing 7 changed files with 232 additions and 74 deletions.
23 changes: 23 additions & 0 deletions dev/gitlab.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
gitlab_rails['initial_root_password']='mK9JnG7jwYdFcBNoQ3W3'
registry['enable']=false
grafana['enable']=false
prometheus_monitoring['enable']=false
gitlab_rails['initial_license_file']='/etc/gitlab/Gitlab.gitlab-license'
gitlab_kas['enable']=false
gitlab_rails['omniauth_enabled'] = true
gitlab_rails['omniauth_allow_single_sign_on'] = ['saml']
gitlab_rails['omniauth_block_auto_created_users'] = false
gitlab_rails['omniauth_auto_link_saml_user'] = true
gitlab_rails['omniauth_providers'] = [
{
name: 'saml',
args: {
assertion_consumer_service_url: 'http://localhost/users/auth/saml/callback',
idp_cert_fingerprint: '11:9B:9E:02:79:59:CD:B7:C6:62:CF:D0:75:D9:E2:EF:38:4E:44:5F',
idp_sso_target_url: 'http://localhost:8080/simplesaml/saml2/idp/SSOService.php',
issuer: 'http://app.example.com',
name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent'
},
label: 'SAML Login'
}
]
172 changes: 98 additions & 74 deletions dev/run_gitlab_in_docker.sh
Original file line number Diff line number Diff line change
@@ -1,35 +1,51 @@
#!/bin/bash

set -euo pipefail

# based on https://stackoverflow.com/a/28709668/2693875 and https://stackoverflow.com/a/23006365/2693875
#!/usr/bin/env bash

# ============================================================================
# GitLab Deployment Script
#
# This script pulls the specified GitLab Docker image, stops and removes any
# existing GitLab container, configures the necessary volumes and environment,
# and starts a new GitLab container with predefined settings.
#
# It also generates files with the GitLab URL and access token for use in
# other scripts or tests.
# ============================================================================

# ============================================================================
# Functions
# ============================================================================

# Print colored output
cecho() {
local color=$1 text=$2

if [[ $TERM == "dumb" ]]; then
echo $2
else
local color=$1
local exp=$2
if ! [[ $color =~ ^[0-9]$ ]] ; then
case $(echo "$color" | tr '[:upper:]' '[:lower:]') in
bk | black) color=0 ;;
r | red) color=1 ;;
g | green) color=2 ;;
y | yellow) color=3 ;;
b | blue) color=4 ;;
m | magenta) color=5 ;;
c | cyan) color=6 ;;
w | white|*) color=7 ;; # white or invalid color
esac
fi
tput setaf $color
# shellcheck disable=SC2086
echo $exp
tput sgr0
echo "$text"
return
fi

case $(echo "$color" | tr '[:upper:]' '[:lower:]') in
bk | black) color=0 ;;
r | red) color=1 ;;
g | green) color=2 ;;
y | yellow) color=3 ;;
b | blue) color=4 ;;
m | magenta)color=5 ;;
c | cyan) color=6 ;;
w | white | *) color=7 ;; # white or invalid color
esac

tput setaf "$color"
echo -e "$text"
tput sgr0
}

script_directory="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
repo_root_directory="$script_directory/.."
# ============================================================================
# Variables
# ============================================================================

script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd)
repo_root_dir="$script_dir/.."

if [[ $# == 1 ]] ; then
gitlab_version="$1"
Expand All @@ -43,88 +59,96 @@ else
gitlab_image="gitlab/gitlab-ee"
fi

# ============================================================================
# Main Logic
# ============================================================================

# Pull the GitLab Docker image
cecho b "Pulling GitLab image version '$gitlab_version'..."
docker pull $gitlab_image:$gitlab_version
docker pull "$gitlab_image:$gitlab_version"

# Stop and remove any existing GitLab container
cecho b "Preparing to start GitLab..."
existing_gitlab_container_id=$(docker ps -a -f "name=gitlab" --format "{{.ID}}")
if [[ -n $existing_gitlab_container_id ]] ; then
existing_container_id=$(docker ps -a -f "name=gitlab" --format "{{.ID}}")
if [[ -n $existing_container_id ]]; then
cecho b "Stopping and removing existing GitLab container..."
docker stop --time=30 "$existing_gitlab_container_id"
docker rm "$existing_gitlab_container_id"
docker stop --time=30 "$existing_container_id"
docker rm "$existing_container_id"
fi

gitlab_omnibus_config="gitlab_rails['initial_root_password'] = 'mK9JnG7jwYdFcBNoQ3W3'; registry['enable'] = false; grafana['enable'] = false; prometheus_monitoring['enable'] = false;"

if [[ -f $repo_root_directory/Gitlab.gitlab-license || -n "${GITLAB_EE_LICENSE:-}" ]] ; then
# Configure volumes and environment
if [[ -f "$repo_root_dir/Gitlab.gitlab-license" || -n "${GITLAB_EE_LICENSE:-}" ]]; then
mkdir -p "$repo_root_dir/config"
rm -rf "$repo_root_dir/config/*"

mkdir -p $repo_root_directory/config
rm -rf $repo_root_directory/config/*

if [[ -f $repo_root_directory/Gitlab.gitlab-license ]]; then
if [[ -f "$repo_root_dir/Gitlab.gitlab-license" ]]; then
cecho b "EE license file found - using it..."
cp $repo_root_directory/Gitlab.gitlab-license $repo_root_directory/config/
cp "$repo_root_dir/Gitlab.gitlab-license" "$repo_root_dir/config/"
else
cecho b "EE license env variable found - using it..."
echo "$GITLAB_EE_LICENSE" > $repo_root_directory/config/Gitlab.gitlab-license
echo "$GITLAB_EE_LICENSE" > "$repo_root_dir/config/Gitlab.gitlab-license"
fi

gitlab_omnibus_config="$gitlab_omnibus_config gitlab_rails['initial_license_file'] = '/etc/gitlab/Gitlab.gitlab-license';"
config_volume="--volume $repo_root_directory/config:/etc/gitlab"
config_volume="--volume $repo_root_dir/config:/etc/gitlab"
else
config_volume=""
fi

# Start a new GitLab container
cecho b "Starting GitLab..."
# run GitLab with root password pre-set and as many unnecessary features disabled to speed up the startup
docker run --detach \
--hostname localhost \
--env GITLAB_OMNIBUS_CONFIG="$gitlab_omnibus_config" \
--publish 443:443 --publish 80:80 --publish 2022:22 \
--name gitlab \
--restart always \
--volume "$repo_root_directory/dev/healthcheck-and-setup.sh:/healthcheck-and-setup.sh" \
$config_volume \
--health-cmd '/healthcheck-and-setup.sh' \
--health-interval 2s \
--health-timeout 2m \
$gitlab_image:$gitlab_version

cecho b "Waiting 2 minutes before starting to check if GitLab has started..."
cecho b "(Run this in another terminal you want to follow the instance logs:"
--hostname localhost \
--publish 443:443 --publish 80:80 --publish 2022:22 \
--name gitlab \
--restart always \
--volume "$repo_root_dir/dev/healthcheck-and-setup.sh:/healthcheck-and-setup.sh" \
--volume "$repo_root_dir/dev/gitlab.rb:/etc/gitlab/gitlab.rb" \
$config_volume \
--health-cmd '/healthcheck-and-setup.sh' \
--health-interval 2s \
--health-timeout 2m \
"$gitlab_image:$gitlab_version"

cecho b "Waiting 2 minutes before checking if GitLab has started..."
cecho b "(Run this in another terminal to follow the instance logs:"
cecho y "docker logs -f gitlab"
cecho b ")"
sleep 120

$script_directory/await-healthy.sh
"$script_dir/await-healthy.sh"

# create files with params needed by the tests to access GitLab
# (we are using these files to pass values from this script to the outside bash shell
# - we cannot change its env variables from inside it)
echo "http://localhost" > $repo_root_directory/gitlab_url.txt
echo "token-string-here123" > $repo_root_directory/gitlab_token.txt
# Generate files with GitLab URL and access token
echo "http://localhost" > "$repo_root_dir/gitlab_url.txt"
echo "token-string-here123" > "$repo_root_dir/gitlab_token.txt"

cecho b 'Starting GitLab complete!'
# Display GitLab information
cecho b 'GitLab started successfully!'
echo ''
cecho b 'GitLab version:'
curl -s -H "Authorization:Bearer $(cat $repo_root_directory/gitlab_token.txt)" http://localhost/api/v4/version
curl -s -H "Authorization:Bearer $(cat "$repo_root_dir/gitlab_token.txt")" http://localhost/api/v4/version
echo ''
if [[ -f $repo_root_directory/Gitlab.gitlab-license || -n "${GITLAB_EE_LICENSE:-}" ]] ; then

if [[ -f "$repo_root_dir/Gitlab.gitlab-license" || -n "${GITLAB_EE_LICENSE:-}" ]]; then
cecho b 'GitLab license (plan, is expired?):'
curl -s -H "Authorization:Bearer $(cat $repo_root_directory/gitlab_token.txt)" http://localhost/api/v4/license | jq '.plan, .expired'
curl -s -H "Authorization:Bearer $(cat "$repo_root_dir/gitlab_token.txt")" http://localhost/api/v4/license | jq '.plan, .expired'
echo ''
fi

cecho b 'GitLab web UI URL (user: root, password: mK9JnG7jwYdFcBNoQ3W3 )'
echo 'http://localhost'
echo ''
cecho b 'You can run these commands to stop and delete the GitLab container:'

# Provide instructions for stopping and starting GitLab
cecho b 'To stop and delete the GitLab container, run:'
cecho r "docker stop --time=30 gitlab"
cecho r "docker rm gitlab"
echo ''
cecho b 'To start GitLab container again, re-run this script. Note that GitLab will NOT keep any data'
cecho b 'so the start will take a lot of time again. (But this is the only way to make GitLab in Docker stable.)'
echo ''
cecho b 'Run this to start the acceptance tests (it will automatically load GITLAB_URL from gitlab_url.txt'
cecho b 'and GITLAB_TOKEN from gitlab_token.txt created by this script):'

cecho b 'To start GitLab container again, re-run this script.'
cecho b 'Note: GitLab will NOT keep any data, so the start will take time.'
cecho b '(This is the only way to make GitLab in Docker stable.)'
echo ''

cecho b 'To start the acceptance tests, run:'
cecho y 'pytest tests/acceptance'
echo ''
1 change: 1 addition & 0 deletions docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ GitLabForm enables you to manage the [(GitLab's) Application Settings](reference
* [Members using LDAP Group Links](reference/group_ldap_links.md) (**GitLab Premium (paid) only**),
* [CI/CD variables](reference/ci_cd_variables.md#group-cicd-variables),
* [Settings](reference/settings.md#group-settings),
* [Members using SAML Group Links](reference/group_saml_links.md) (**GitLab Premium (paid) only**),

* Project:
* [Archive/unarchive](reference/archive_unarchive.md),
Expand Down
33 changes: 33 additions & 0 deletions docs/reference/group_saml_links.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Group SAML links

!!! info

This section requires GitLab Premium (paid). (This is a GitLab's limitation, not GitLabForm's.)

This section purpose is to manage [group membership via SAML group links](https://docs.gitlab.com/ee/user/group/saml_sso/group_sync.html#configure-saml-group-links).

Key names here are just any labels.

Except if the key name is `enforce` and is set to `true` - then only the group SAML links defined here will remain in the group, all other will be deleted.

Values are like documented at [SAML Group Links section of the Groups API docs](https://docs.gitlab.com/ee/api/groups.html#saml-group-links), **except the id**.

The `saml_group_name` should be set to the SAML group name

The `access_level` should be set to one of the [valid access levels](https://docs.gitlab.com/ee/api/members.html#valid-access-levels).

Example:

```yaml
projects_and_groups:
group_1/*:
saml_group_links:
devops_are_maintainers: # this is just a label
saml_group_name: devops
access_level: maintainer
developers_are_developers: # this is just a label
saml_group_name: developers
access_level: developer

enforce: true # optional
```
2 changes: 2 additions & 0 deletions gitlabform/processors/group/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from gitlabform.processors.group.group_members_processor import (
GroupMembersProcessor,
)
from gitlabform.processors.group.group_saml_links_processor import GroupSAMLLinksProcessor
from gitlabform.processors.group.group_variables_processor import (
GroupVariablesProcessor,
)
Expand All @@ -31,5 +32,6 @@ def __init__(self, gitlab: GitLab, config: Configuration, strict: bool):
GroupMembersProcessor(gitlab),
GroupLDAPLinksProcessor(gitlab),
GroupBadgesProcessor(gitlab),
GroupSAMLLinksProcessor(gitlab),
GroupLabelsProcessor(gitlab),
]
60 changes: 60 additions & 0 deletions gitlabform/processors/group/group_saml_links_processor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
from logging import debug
from typing import List


from gitlabform.gitlab import GitLab
from gitlab.base import RESTObject, RESTObjectList
from gitlab.v4.objects import Group
from gitlabform.processors.abstract_processor import AbstractProcessor


class GroupSAMLLinksProcessor(AbstractProcessor):

def __init__(self, gitlab: GitLab):
super().__init__("saml_group_links", gitlab)

def _process_configuration(self, group_path: str, configuration: dict) -> None:
"""Process the SAML links configuration for a group."""

configured_links = configuration.get("saml_group_links", {})
enforce_links = configuration.get("saml_group_links|enforce", False)

group: Group = self.gl.get_group_by_path_cached(group_path)
existing_links: RESTObjectList | List[RESTObject] = self._fetch_saml_links(
group
)
existing_link_names = [existing_link.name for existing_link in existing_links]

# Remove 'enforce' key from the config so that it's not treated as a "link"
if enforce_links:
configured_links.pop("enforce")

for link_name, link_configuration in configured_links.items():
if link_name not in existing_link_names:
group.saml_group_links.create(link_configuration)
group.save()

if enforce_links:
self._delete_extra_links(group, existing_links, configured_links)

def _fetch_saml_links(self, group: Group) -> RESTObjectList | List[RESTObject]:
"""Fetch the existing SAML links for a group."""
return group.saml_group_links.list()

def _delete_extra_links(
self,
group: Group,
existing: RESTObjectList | List[RESTObject],
configured: dict,
) -> None:
"""Delete any SAML links that are not in the configuration."""
known_names = [
common_name["name"]
for common_name in configured.values()
if common_name != "enforce"
]

for link in existing:
if link.name not in known_names:
debug(f"Deleting extra SAML link: {link.name}")
group.saml_group_links.delete(link.id)
15 changes: 15 additions & 0 deletions tests/acceptance/premium/test_group_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,18 @@ def test__edit_new_setting_premium(self, gl, project, group):

refreshed_group = gl.groups.get(group.id)
assert refreshed_group.file_template_project_id == project.id

def test__add_saml_links_premium(self, gl, project, group):

assert len(group.saml_group_links.list()) == 0, "saml_group_links is not empty"
add_group_saml_settings = f"""
projects_and_groups:
{group.full_path}/*:
saml_group_links:
devops_are_maintainers:
saml_group_name: devops_maintainer,
access_level: maintainer
"""
run_gitlabform(add_group_saml_settings, group)
refreshed_group = gl.groups.get(group.id)
assert len(refreshed_group.saml_group_links.list()) == 1

0 comments on commit e2a44dc

Please sign in to comment.