1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
|
import aiohttp
import discord
from discord.ext import commands, tasks
from arthur.apis import grafana
from arthur.apis.directory import ldap
from arthur.bot import KingArthur
from arthur.config import CONFIG
from arthur.log import logger
from . import MissingMembers, SyncFigures
GRAFANA_TO_LDAP_NAME_MAPPING = {
"devops": "devops",
"admins": "administrators",
"moderators": "moderators",
"core-developers": "coredevs",
}
class GrafanaLDAPTeamSync(commands.Cog):
"""
Update Grafana team membership to match LDAP team membership.
Whilst the LDAP migration is ongoing, we re-map the LDAP group names to the Grafana teams,
in future they will be unified.
"""
def __init__(self, bot: KingArthur) -> None:
self.bot = bot
self.sync_ldap_grafana_teams.start()
async def _add_missing_members(
self,
grafana_team_id: int,
ldap_team_members: set[str],
grafana_team_members: set[str],
all_grafana_users: list[dict],
) -> MissingMembers:
"""
Adds members to the Grafana team if they're in the LDAP team and not already present.
Returns the number of missing members, and the number of members it could actually add.
"""
missing_members = ldap_team_members - grafana_team_members
added_members = 0
for grafana_user in all_grafana_users:
if grafana_user["login"] not in missing_members:
continue
if "LDAP" not in grafana_user.get("authLabels", []):
continue
await grafana.add_user_to_team(
grafana_user["userId"],
grafana_team_id,
self.bot.http_session,
)
added_members += 1
return MissingMembers(count=len(missing_members), successfully_added=added_members)
async def _remove_extra_members(
self,
grafana_team_id: int,
ldap_team_members: set[str],
grafana_team_members: set[str],
all_grafana_users: list[dict],
) -> int:
"""
Removes Grafana users from a team if they are not present in the LDAP team.
Return how many were removed.
"""
extra_members = grafana_team_members - ldap_team_members
removed_members = 0
for grafana_user in all_grafana_users:
if grafana_user["login"] not in extra_members:
continue
await grafana.remove_user_from_team(
grafana_user["userId"],
grafana_team_id,
self.bot.http_session,
)
removed_members += 1
return removed_members
async def _sync_teams(self, team: dict[str, str]) -> SyncFigures:
"""
Ensure members in LDAP are present in Grafana teams.
Return the number of members missing from the Grafana team, and the number of members added.
"""
if team["name"] not in GRAFANA_TO_LDAP_NAME_MAPPING:
return SyncFigures(added=MissingMembers(0, 0), removed=0)
ldap_team_members = {
member.uid
for member in await ldap.get_group_members(GRAFANA_TO_LDAP_NAME_MAPPING[team["name"]])
}
grafana_team_members = {
member["login"]
for member in await grafana.list_team_members(team["id"], self.bot.http_session)
if member.get("auth_module") == "ldap"
}
all_grafana_users = await grafana.get_all_users(self.bot.http_session)
added_members = await self._add_missing_members(
team["id"],
ldap_team_members,
grafana_team_members,
all_grafana_users,
)
removed_members = await self._remove_extra_members(
team["id"],
ldap_team_members,
grafana_team_members,
all_grafana_users,
)
return SyncFigures(added=added_members, removed=removed_members)
@tasks.loop(hours=12)
async def sync_ldap_grafana_teams(self, channel: discord.TextChannel | None = None) -> None:
"""Update Grafana team membership to match LDAP team membership."""
grafana_teams = await grafana.list_teams(self.bot.http_session)
embed = discord.Embed(
title="Sync Stats",
colour=discord.Colour.blue(),
)
for team in grafana_teams:
logger.debug(f"Processing {team['name']}")
try:
figures = await self._sync_teams(team)
except aiohttp.ClientResponseError as e:
logger.opt(exception=e).error(f"Error whilst procesing Grafana team {team['name']}")
if channel:
await channel.send(e)
continue
lines = [
f"Missing: {figures.added.count}",
f"Added: {figures.added.successfully_added}",
f"Removed: {figures.removed}",
]
embed.add_field(
name=team["name"],
value="\n".join(lines),
inline=False,
)
if channel:
await channel.send(embed=embed)
@sync_ldap_grafana_teams.error
async def on_task_error(self, error: Exception) -> None:
"""Ensure task errors are output."""
logger.error(error)
@commands.group(name="grafana_ldap", invoke_without_command=True)
async def grafana_group(self, ctx: commands.Context) -> None:
"""Commands for working with grafana API."""
await ctx.send_help(ctx.command)
@grafana_group.command(name="sync")
async def sync_teams(self, ctx: commands.Context) -> None:
"""Sync Grafana & LDAP teams now."""
await self.sync_ldap_grafana_teams(ctx.channel)
async def setup(bot: KingArthur) -> None:
"""Add cog to bot."""
if ldap.BONSAI_AVAILABLE and CONFIG.enable_ldap and CONFIG.grafana_token:
await bot.add_cog(GrafanaLDAPTeamSync(bot))
else:
logger.warning(
"Not loading Grafana LDAP team sync as LDAP dependencies "
"LDAP dependencies are not installed, LDAP is disabled,"
" or grafana_token not set. See README.md for more"
)
|