diff --git a/README.md b/README.md
index 2ac5c47..7704eac 100644
--- a/README.md
+++ b/README.md
@@ -142,11 +142,11 @@ plugin system**.
 There is still a lot in store for RepoBee. Below is a roadmap for major
 features that are in the works.
 
-| Feature                 | Status                                                                                      | ETA         |
-| -------                 | ------                                                                                      | ---         |
-| Full support for GitLab | Work in progress ([See docs](https://repobee.readthedocs.io/en/latest/gitlab.html#roadmap)) | Late 2019   |
-| Video tutorials         | Planning                                                                                    | August 2019 |
-| GUI                     | Planning                                                                                    | Late 2019   |
+| Feature                 | Status                                                                                      | ETA            |
+| -------                 | ------                                                                                      | ---            |
+| Full support for GitLab | Work in progress ([See docs](https://repobee.readthedocs.io/en/latest/gitlab.html#roadmap)) | September 2019 |
+| Video tutorials         | Work in progress                                                                            | September 2019 |
+| GUI                     | Planning                                                                                    | Late 2019      |
 
 ## License
 This software is licensed under the MIT License. See the [LICENSE](LICENSE)
diff --git a/docs/gitlab.rst b/docs/gitlab.rst
index c5aa109..71c016b 100644
--- a/docs/gitlab.rst
+++ b/docs/gitlab.rst
@@ -38,7 +38,7 @@ list-issues          Done           v1.6.0
 assign-reviews       Not started    Late 2019
 end-reviews          Not started    Late 2019
 check-reviews        Not started    Late 2019
-verify-settings      Not started    August 2019
+verify-settings      Not started    v2.3.0
 ===================  =============  ============
 
 GitLab terminology
diff --git a/src/_repobee/cli.py b/src/_repobee/cli.py
index e155c59..fbb2143 100644
--- a/src/_repobee/cli.py
+++ b/src/_repobee/cli.py
@@ -172,6 +172,7 @@ def parse_args(
 
     _validate_tls_url(args.base_url)
 
+    user = args.user if "user" in args else None
     if subparser == VERIFY_PARSER:
         # quick parse for verify connection
         return (
@@ -179,7 +180,7 @@ def parse_args(
                 subparser=VERIFY_PARSER,
                 org_name=args.org_name,
                 base_url=args.base_url,
-                user=args.user,
+                user=user,
                 traceback=args.traceback,
                 master_org_name=args.master_org_name
                 if "master_org_name" in args
@@ -192,12 +193,7 @@ def parse_args(
         # only if clone is chosen should plugins be able to hook in
         plug.manager.hook.parse_args(args=args)
 
-    api = _connect_to_api(
-        args.base_url,
-        args.token,
-        args.org_name,
-        args.user if "user" in args else None,
-    )
+    api = _connect_to_api(args.base_url, args.token, args.org_name, user)
 
     master_org_name = args.org_name
     if "master_org_name" in args and args.master_org_name is not None:
diff --git a/src/_repobee/ext/gitlab.py b/src/_repobee/ext/gitlab.py
index 4742895..8b51600 100644
--- a/src/_repobee/ext/gitlab.py
+++ b/src/_repobee/ext/gitlab.py
@@ -19,6 +19,7 @@ from typing import List, Iterable, Optional, Generator, Tuple
 
 import daiquiri
 import gitlab
+import requests.exceptions
 
 import repobee_plug as plug
 
@@ -57,6 +58,14 @@ def _convert_404_to_not_found_error(msg):
         )
 
 
+@contextlib.contextmanager
+def _convert_error(expected, conversion, msg):
+    try:
+        yield
+    except expected as exc:
+        raise conversion(msg) from exc
+
+
 @contextlib.contextmanager
 def _try_api_request(ignore_statuses: Optional[Iterable[int]] = None):
     """Context manager for trying API requests.
@@ -91,12 +100,9 @@ class GitLabAPI(plug.API):
 
     def __init__(self, base_url, token, org_name):
         # ssl turns off only for
-        ssl_verify = not os.getenv("REPOBEE_NO_VERIFY_SSL") == "true"
-        if not ssl_verify:
-            LOGGER.warning("SSL verification turned off, only for testing")
         self._user = "oauth2"
         self._gitlab = gitlab.Gitlab(
-            base_url, private_token=token, ssl_verify=ssl_verify
+            base_url, private_token=token, ssl_verify=self._ssl_verify()
         )
         self._group_name = org_name
         self._token = token
@@ -105,6 +111,13 @@ class GitLabAPI(plug.API):
         with _try_api_request():
             self._group = self._get_organization(self._group_name)
 
+    @staticmethod
+    def _ssl_verify():
+        ssl_verify = not os.getenv("REPOBEE_NO_VERIFY_SSL") == "true"
+        if not ssl_verify:
+            LOGGER.warning("SSL verification turned off, only for testing")
+        return ssl_verify
+
     def _get_teams_in(self, team_names: Iterable[str]):
         """Get all teams that match any team name in the team_names iterable.
 
@@ -447,6 +460,88 @@ class GitLabAPI(plug.API):
             )
             yield from name_issues_pairs
 
+    @staticmethod
+    def verify_settings(
+        user: str,
+        org_name: str,
+        base_url: str,
+        token: str,
+        master_org_name: Optional[str] = None,
+    ):
+        """See :py:meth:`repobee_plug.apimeta.APISpec.verify_settings`."""
+        LOGGER.info("GitLabAPI is verifying settings ...")
+        if not token:
+            raise exception.BadCredentials(
+                msg="Token is empty. Check that REPOBEE_TOKEN environment "
+                "variable is properly set, or supply the `--token` option."
+            )
+
+        gl = gitlab.Gitlab(
+            base_url, private_token=token, ssl_verify=GitLabAPI._ssl_verify()
+        )
+
+        LOGGER.info("Authenticating connection to {}...".format(base_url))
+        with _convert_error(
+            gitlab.exceptions.GitlabAuthenticationError,
+            exception.BadCredentials,
+            "Could not authenticate token",
+        ), _convert_error(
+            requests.exceptions.ConnectionError,
+            exception.APIError,
+            "Could not connect to {}, please check the URL".format(base_url),
+        ):
+            gl.auth()
+        LOGGER.info(
+            "SUCCESS: Authenticated as {} at {}".format(
+                gl.user.username, base_url
+            )
+        )
+
+        GitLabAPI._verify_group(org_name, gl)
+        if master_org_name:
+            GitLabAPI._verify_group(master_org_name, gl)
+
+        LOGGER.info("GREAT SUCCESS: All settings check out!")
+
+    @staticmethod
+    def _verify_group(group_name: str, gl: gitlab.Gitlab) -> None:
+        """Check that the group exists and that the user is an owner."""
+        user = gl.user.username
+
+        LOGGER.info("Trying to fetch group {}".format(group_name))
+        slug_matched = [
+            group
+            for group in gl.groups.list(search=group_name)
+            if group.path == group_name
+        ]
+        if not slug_matched:
+            raise exception.NotFoundError(
+                "Could not find group with slug {}. Verify that you have "
+                "access to the group, and that you've provided the slug "
+                "(the name in the address bar).".format(group_name)
+            )
+        group = slug_matched[0]
+        LOGGER.info("SUCCESS: Found group {}".format(group.name))
+
+        LOGGER.info(
+            "Verifying that user {} is an owner of group {}".format(
+                user, group_name
+            )
+        )
+        matching_members = [
+            member
+            for member in group.members.list()
+            if member.username == user
+            and member.access_level == gitlab.OWNER_ACCESS
+        ]
+        if not matching_members:
+            raise exception.BadCredentials(
+                "User {} is not an owner of {}".format(user, group_name)
+            )
+        LOGGER.info(
+            "SUCCESS: User {} is an owner of group {}".format(user, group_name)
+        )
+
 
 class GitLabAPIHook(plug.Plugin):
     def api_init_requires(self):

Implement verify-settings for GitLab
Requires some re-writing in the cli module, as well as implementing the required function in the GitLabAPI class.
diff --git a/tests/unit_tests/plugin_tests/test_gitlab.py b/tests/unit_tests/plugin_tests/test_gitlab.py
index 4ebfed9..3fb1c4b 100644
--- a/tests/unit_tests/plugin_tests/test_gitlab.py
+++ b/tests/unit_tests/plugin_tests/test_gitlab.py
@@ -1,5 +1,6 @@
 from collections import namedtuple
 
+import requests.exceptions
 import pytest
 import gitlab
 import repobee_plug as plug
@@ -25,11 +26,11 @@ class Group:
             create=self._create_member, list=self._list_members
         )
         self._member_list = []
-        self._users_read_only = users_read_only
+        self._users_read_only = users_read_only or {}
         # the group owner is always added to the group with max access level
         owner_id = [
             uid
-            for uid, user in users_read_only.items()
+            for uid, user in self._users_read_only.items()
             if user.username == constants.USER
         ][0]
         owner_data = {"user_id": owner_id, "access_level": gitlab.OWNER_ACCESS}
@@ -72,6 +73,9 @@ class Project:
         self.attributes = dict(http_url_to_repo=http_url)
 
 
+User = namedtuple("User", ("id", "username"))
+
+
 class GitLabMock:
     """Class representing a GitLab instance, with the subset of functionality
     required for RepoBee to work. It may seem counterintuitive to create this
@@ -85,14 +89,18 @@ class GitLabMock:
 
     _Groups = namedtuple("_Groups", ("create", "list", "get"))
     _Users = namedtuple("_Users", ("list"))
-    _User = namedtuple("_User", ("id", "username"))
     _Projects = namedtuple("_Projects", "create get".split())
 
     def __init__(self, url, private_token, ssl_verify):
         self._users = {
-            id: self._User(id=id, username=str(grp))
+            id: User(id=id, username=str(grp))
             for id, grp in enumerate(constants.STUDENTS + (constants.USER,))
         }
+        self._owner, *_ = [
+            usr
+            for usr in self._users.values()
+            if usr.username == constants.USER
+        ]
         self._base_url = url
         self._private_token = private_token
         self._groups = {}
@@ -103,6 +111,18 @@ class GitLabMock:
         # this is only for testing purposes, does not exist in the real class
         self._target_group_id = list(self._groups.keys())[0]
 
+    def auth(self):
+        if self._base_url != BASE_URL:
+            raise requests.exceptions.ConnectionError("could not connect")
+        if self._private_token != TOKEN:
+            raise gitlab.exceptions.GitlabAuthenticationError(
+                "could not authenticate token"
+            )
+
+    @property
+    def user(self):
+        return self._owner
+
     @property
     def tests_only_target_group_id(self):
         return self._target_group_id
@@ -243,6 +263,7 @@ class GitLabMock:
 BASE_URL = "https://some-host.com"
 TOKEN = "3049fplktdufpdl23"
 TARGET_GROUP = "repobee-testing"
+MASTER_GROUP = "repobee-master"
 
 
 @pytest.fixture(autouse=True)
@@ -252,6 +273,11 @@ def api_mock(mocker):
     )
 
 
+@pytest.fixture
+def api(api_mock):
+    yield _repobee.ext.gitlab.GitLabAPI(BASE_URL, TOKEN, TARGET_GROUP)
+
+
 @pytest.fixture
 def team_names():
     return [str(s) for s in constants.STUDENTS]
@@ -483,10 +509,6 @@ class TestGetRepoUrls:
 
 
 class TestCreateRepos:
-    @pytest.fixture
-    def api(self, api_mock):
-        yield _repobee.ext.gitlab.GitLabAPI(BASE_URL, TOKEN, TARGET_GROUP)
-
     @pytest.fixture
     def repos(self, api, master_repo_names):
         """Setup repo tuples along with groups for the repos to be created
@@ -535,3 +557,86 @@ class TestCreateRepos:
         actual_urls = api.create_repos(repos)
 
         assert sorted(expected_urls) == sorted(actual_urls)
+
+
+class TestVerifySettings:
+    def test_raises_if_token_is_empty(self):
+        with pytest.raises(exception.BadCredentials):
+            _repobee.ext.gitlab.GitLabAPI.verify_settings(
+                user=None, org_name=TARGET_GROUP, base_url=BASE_URL, token=""
+            )
+
+    def test_raises_on_failed_connection(self):
+        with pytest.raises(exception.APIError) as exc_info:
+            _repobee.ext.gitlab.GitLabAPI.verify_settings(
+                user=None,
+                org_name=TARGET_GROUP,
+                base_url="https://garbage-url",
+                token=TOKEN,
+            )
+
+        assert "please check the URL" in str(exc_info.value)
+
+    def test_raises_on_bad_token(self):
+        with pytest.raises(exception.BadCredentials) as exc_info:
+            _repobee.ext.gitlab.GitLabAPI.verify_settings(
+                user=None,
+                org_name=TARGET_GROUP,
+                base_url=BASE_URL,
+                token="wrong-token",
+            )
+
+        assert "Could not authenticate token" in str(exc_info.value)
+
+    def test_raises_if_group_cant_be_found(self):
+        non_existing_group = "some-garbage-group"
+        with pytest.raises(exception.NotFoundError) as exc_info:
+            _repobee.ext.gitlab.GitLabAPI.verify_settings(
+                user=None,
+                org_name=non_existing_group,
+                base_url=BASE_URL,
+                token=TOKEN,
+            )
+
+        assert "Could not find group with slug {}".format(
+            non_existing_group
+        ) in str(exc_info.value)
+
+    def test_raises_if_master_group_cant_be_found(self):
+        non_existing_group = "some-garbage-group"
+        with pytest.raises(exception.NotFoundError) as exc_info:
+            _repobee.ext.gitlab.GitLabAPI.verify_settings(
+                user=None,
+                org_name=TARGET_GROUP,
+                base_url=BASE_URL,
+                token=TOKEN,
+                master_org_name=non_existing_group,
+            )
+
+        assert "Could not find group with slug {}".format(
+            non_existing_group
+        ) in str(exc_info.value)
+
+    def test_happy_path(self, mocker):
+        """Test that the great success message is printed if all is as it
+        should.
+        """
+        gl = GitLabMock(BASE_URL, TOKEN, False)
+        gl.groups.create(dict(name=MASTER_GROUP, path=MASTER_GROUP))
+        mocker.patch(
+            "_repobee.ext.gitlab.gitlab.Gitlab",
+            side_effect=lambda base_url, private_token, ssl_verify: gl,
+        )
+        log_mock = mocker.patch("_repobee.ext.gitlab.LOGGER")
+
+        _repobee.ext.gitlab.GitLabAPI.verify_settings(
+            user=None,
+            org_name=TARGET_GROUP,
+            base_url=BASE_URL,
+            token=TOKEN,
+            master_org_name=MASTER_GROUP,
+        )
+
+        log_mock.info.assert_called_with(
+            "GREAT SUCCESS: All settings check out!"
+        )

pip install -e .[TEST]
parse_log_pytest
requirements.txt
pytest pytest-cov pytest-mock codecov
apt-get update apt-get install -y gcc
3.7
requirements/app.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
appdirs==1.4.4
backports.zoneinfo==0.2.1
certifi @ file:///croot/certifi_1671487769961/work/certifi
cffi==1.15.1
charset-normalizer==3.4.1
codecov==2.1.13
colored==1.4.4
coverage==7.2.7
cryptography==44.0.2
daiquiri==3.2.1
dateparser==1.2.0
Deprecated==1.2.18
exceptiongroup==1.2.2
humanize==4.6.0
idna==3.10
importlib-metadata==6.7.0
iniconfig==2.0.0
maya==0.6.1
packaging==24.0
pendulum==2.1.2
pluggy==1.2.0
pycparser==2.21
PyGithub==2.3.0
PyJWT==2.8.0
PyNaCl==1.5.0
pytest==7.4.4
pytest-cov==4.1.0
pytest-mock==3.11.1
python-dateutil==2.9.0.post0
python-gitlab==1.9.0
python-json-logger==3.0.1
pytz==2025.2
pytzdata==2020.1
regex==2024.4.16
-e git+https://github.com/repobee/repobee.git@b706b5d9a283305d53af92c7a0bf641fcb12126c#egg=repobee
repobee-plug==0.10.0
requests==2.31.0
six==1.17.0
snaptime==0.2.4
tomli==2.0.1
typing_extensions==4.7.1
tzlocal==5.1
urllib3==2.0.7
wrapt==1.16.0
zipp==3.15.0

name: repobee
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2022.12.7=py37h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=22.3.1=py37h06a4308_0
  - python=3.7.16=h7a1cb2a_0
  - readline=8.2=h5eee18b_0
  - setuptools=65.6.3=py37h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.38.4=py37h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - appdirs==1.4.4
      - backports-zoneinfo==0.2.1
      - cffi==1.15.1
      - charset-normalizer==3.4.1
      - codecov==2.1.13
      - colored==1.4.4
      - coverage==7.2.7
      - cryptography==44.0.2
      - daiquiri==3.2.1
      - dateparser==1.2.0
      - deprecated==1.2.18
      - exceptiongroup==1.2.2
      - humanize==4.6.0
      - idna==3.10
      - importlib-metadata==6.7.0
      - iniconfig==2.0.0
      - maya==0.6.1
      - packaging==24.0
      - pendulum==2.1.2
      - pluggy==1.2.0
      - pycparser==2.21
      - pygithub==2.3.0
      - pyjwt==2.8.0
      - pynacl==1.5.0
      - pytest==7.4.4
      - pytest-cov==4.1.0
      - pytest-mock==3.11.1
      - python-dateutil==2.9.0.post0
      - python-gitlab==1.9.0
      - python-json-logger==3.0.1
      - pytz==2025.2
      - pytzdata==2020.1
      - regex==2024.4.16
      - repobee==2.2.0
      - repobee-plug==0.10.0
      - requests==2.31.0
      - six==1.17.0
      - snaptime==0.2.4
      - tomli==2.0.1
      - typing-extensions==4.7.1
      - tzlocal==5.1
      - urllib3==2.0.7
      - wrapt==1.16.0
      - zipp==3.15.0
prefix: /opt/conda/envs/repobee

tests/unit_tests/plugin_tests/test_gitlab.py::TestVerifySettings::test_raises_if_token_is_empty tests/unit_tests/plugin_tests/test_gitlab.py::TestVerifySettings::test_raises_on_failed_connection tests/unit_tests/plugin_tests/test_gitlab.py::TestVerifySettings::test_raises_on_bad_token tests/unit_tests/plugin_tests/test_gitlab.py::TestVerifySettings::test_raises_if_group_cant_be_found tests/unit_tests/plugin_tests/test_gitlab.py::TestVerifySettings::test_raises_if_master_group_cant_be_found tests/unit_tests/plugin_tests/test_gitlab.py::TestVerifySettings::test_happy_path
tests/unit_tests/plugin_tests/test_gitlab.py::TestEnsureTeamsAndMembers::test_converts_gitlab_errors_to_repobee_errors[raised_error0-BadCredentials] tests/unit_tests/plugin_tests/test_gitlab.py::TestEnsureTeamsAndMembers::test_converts_gitlab_errors_to_repobee_errors[raised_error1-NotFoundError] tests/unit_tests/plugin_tests/test_gitlab.py::TestEnsureTeamsAndMembers::test_converts_gitlab_errors_to_repobee_errors[raised_error2-APIError] tests/unit_tests/plugin_tests/test_gitlab.py::TestEnsureTeamsAndMembers::test_with_no_pre_existing_groups tests/unit_tests/plugin_tests/test_gitlab.py::TestEnsureTeamsAndMembers::test_with_multi_student_groups tests/unit_tests/plugin_tests/test_gitlab.py::TestEnsureTeamsAndMembers::test_run_twice tests/unit_tests/plugin_tests/test_gitlab.py::TestEnsureTeamsAndMembers::test_respects_permission tests/unit_tests/plugin_tests/test_gitlab.py::TestGetRepoUrls::test_get_master_repo_urls tests/unit_tests/plugin_tests/test_gitlab.py::TestGetRepoUrls::test_get_master_repo_urls_in_master_group tests/unit_tests/plugin_tests/test_gitlab.py::TestGetRepoUrls::test_get_student_repo_urls tests/unit_tests/plugin_tests/test_gitlab.py::TestCreateRepos::test_run_once_for_valid_repos tests/unit_tests/plugin_tests/test_gitlab.py::TestCreateRepos::test_run_twice_for_valid_repos
src/_repobee/ext/gitlab.py docs/gitlab.rst README.md src/_repobee/cli.py
src/_repobee/ext/gitlab.py docs/gitlab.rst README.md src/_repobee/cli.py