How to make a Rust team auto authorization plugin (TC, locks, turrets)
Automatically authorizes a player on every tool cupboard, code lock and auto turret owned by their team when they join a team, and removes that authorization when they leave, are kicked or the team disbands. It uses OnTeamAcceptInvite, OnTeamLeave, OnTeamKick and OnTeamDisbanded.
Rust teams share a chat, map markers and pings, but they do not share building authorization. When a player joins a team, teammates still have to walk to every tool cupboard and code lock and add the new member by hand, and when someone leaves the team their authorization stays behind on every lock they touched. On a wipe-day server or a large group this is the most common source of "why can't I use my own turret" tickets.
This plugin watches team membership events and mirrors them onto owned entities. On accept it authorizes the new member; on leave, kick and disband it removes authorization. It scans tool cupboards (BuildingPrivlidge), code locks (CodeLock) and auto turrets (AutoTurret) whose OwnerID belongs to a current team member. Owners are never stripped from their own entities, and a per-player opt-out is stored in oxide/data.
It runs entirely off hooks, needs no polling, and its only persistent state is the opt-out list.
The plugin code
using System.Linq;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Plugins;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Team Auto Auth", "RustPluginGenerator", "1.0.0")]
[Description("Authorizes teammates on tool cupboards, code locks and auto turrets when they join a team, and removes them on leave.")]
public class TeamAutoAuth : RustPlugin
{
private const string PermUse = "teamautoauth.use";
private const string PermAdmin = "teamautoauth.admin";
private class PluginConfig
{
[JsonProperty("Enabled by default")]
public bool Enabled { get; set; } = true;
[JsonProperty("Authorize tool cupboards")]
public bool AuthCupboards { get; set; } = true;
[JsonProperty("Authorize code locks")]
public bool AuthLocks { get; set; } = true;
[JsonProperty("Authorize auto turrets")]
public bool AuthTurrets { get; set; } = true;
}
private class StoredData
{
[JsonProperty("Player opt-outs")]
public HashSet<ulong> OptOut { get; set; } = new HashSet<ulong>();
}
private PluginConfig config;
private StoredData data;
private readonly HashSet<ulong> syncedTeams = new HashSet<ulong>();
protected override void LoadDefaultConfig() => config = new PluginConfig();
protected override void LoadConfig()
{
base.LoadConfig();
try
{
config = Config.ReadObject<PluginConfig>();
if (config == null) throw new Exception();
}
catch
{
PrintWarning("Configuration file is invalid, using defaults");
LoadDefaultConfig();
}
SaveConfig();
}
protected override void SaveConfig() => Config.WriteObject(config);
private void LoadData()
{
try
{
data = Interface.Oxide.DataFileSystem.ReadObject<StoredData>(Name);
}
catch
{
data = null;
}
if (data == null) data = new StoredData();
}
private void SaveData() => Interface.Oxide.DataFileSystem.WriteObject(Name, data);
private void Init()
{
permission.RegisterPermission(PermUse, this);
permission.RegisterPermission(PermAdmin, this);
LoadData();
}
protected override void LoadDefaultMessages()
{
lang.RegisterMessages(new Dictionary<string, string>
{
["NoPermission"] = "You do not have permission to use this command.",
["OptedOut"] = "Team auto authorization is now off for you.",
["OptedIn"] = "Team auto authorization is now on for you.",
["StatusOn"] = "Team auto authorization is currently on.",
["StatusOff"] = "Team auto authorization is currently off.",
["Usage"] = "Usage: /teamauth <on|off|status> or teamauth.sync <on|off>",
["Synced"] = "Rescanned {0} team(s) and applied authorization to online members.",
["SyncDefault"] = "Server default is now {0}."
}, this);
}
private string Msg(string key, string userId = null, params object[] args) =>
string.Format(lang.GetMessage(key, this, userId), args);
private bool IsOptedOut(BasePlayer player) =>
data != null && data.OptOut.Contains(player.userID.Get());
private bool WantsAutoAuth(BasePlayer player) => config.Enabled ? !IsOptedOut(player) : IsOptedOut(player);
private object OnTeamAcceptInvite(RelationshipManager.PlayerTeam playerTeam, BasePlayer player)
{
if (player == null || playerTeam == null) return null;
if (!WantsAutoAuth(player)) return null;
AuthorizePlayer(player, playerTeam);
syncedTeams.Add(playerTeam.teamID);
return null;
}
private object OnTeamLeave(RelationshipManager.PlayerTeam playerTeam, BasePlayer player)
{
if (player == null || playerTeam == null) return null;
DeauthorizePlayer(player, playerTeam);
return null;
}
private object OnTeamKick(RelationshipManager.PlayerTeam playerTeam, BasePlayer player, ulong uLong)
{
if (player == null || playerTeam == null) return null;
DeauthorizePlayer(player, playerTeam);
return null;
}
private void OnTeamDisbanded(RelationshipManager.PlayerTeam playerTeam)
{
if (playerTeam == null) return;
foreach (ulong id in new List<ulong>(playerTeam.members))
{
BasePlayer member = BasePlayer.FindByID(id);
if (member != null) DeauthorizePlayer(member, playerTeam);
}
syncedTeams.Remove(playerTeam.teamID);
}
private void AuthorizePlayer(BasePlayer player, RelationshipManager.PlayerTeam team)
{
ApplyToEntities(player, team, true);
}
private void DeauthorizePlayer(BasePlayer player, RelationshipManager.PlayerTeam team)
{
ApplyToEntities(player, team, false);
}
private void ApplyToEntities(BasePlayer player, RelationshipManager.PlayerTeam team, bool grant)
{
if (player == null || team == null || team.members == null || team.members.Count == 0) return;
HashSet<ulong> memberIds = new HashSet<ulong>(team.members);
foreach (BaseNetworkable networkable in BaseNetworkable.serverEntities)
{
BaseEntity entity = networkable as BaseEntity;
if (entity == null || entity.IsDestroyed) continue;
if (!memberIds.Contains(entity.OwnerID)) continue;
if (config.AuthCupboards && entity is BuildingPrivlidge privlidge)
{
if (!grant && privlidge.OwnerID == player.userID.Get()) continue;
if (!grant) privlidge.authorizedPlayers.Remove(player.userID);
else if (!privlidge.IsAuthed(player)) privlidge.authorizedPlayers.Add(player.userID);
privlidge.SendNetworkUpdate();
}
else if (config.AuthLocks && entity is CodeLock codeLock)
{
if (!grant && codeLock.OwnerID == player.userID.Get()) continue;
if (!grant) codeLock.whitelistPlayers.Remove(player.userID);
else if (!codeLock.whitelistPlayers.Contains(player.userID)) codeLock.whitelistPlayers.Add(player.userID);
codeLock.SendNetworkUpdate();
}
else if (config.AuthTurrets && entity is AutoTurret turret)
{
if (!grant && turret.OwnerID == player.userID.Get()) continue;
if (!grant) turret.authorizedPlayers.Remove(player.userID);
else if (!turret.authorizedPlayers.Contains(player.userID)) turret.authorizedPlayers.Add(player.userID);
turret.SendNetworkUpdate();
}
}
}
[ChatCommand("teamauth")]
private void CmdTeamAuth(BasePlayer player, string command, string[] args)
{
if (player == null) return;
if (!permission.UserHasPermission(player.UserIDString, PermUse))
{
SendReply(player, Msg("NoPermission", player.UserIDString));
return;
}
string arg = args != null && args.Length > 0 ? args[0].ToLower() : "status";
if (arg == "on")
{
data.OptOut.Remove(player.userID.Get());
SaveData();
SendReply(player, Msg("OptedIn", player.UserIDString));
}
else if (arg == "off")
{
data.OptOut.Add(player.userID.Get());
SaveData();
SendReply(player, Msg("OptedOut", player.UserIDString));
}
else if (arg == "status")
{
SendReply(player, Msg(WantsAutoAuth(player) ? "StatusOn" : "StatusOff", player.UserIDString));
}
else
{
SendReply(player, Msg("Usage", player.UserIDString));
}
}
[ConsoleCommand("teamauth.sync")]
private void CcmdSync(ConsoleSystem.Arg arg)
{
if (arg == null) return;
BasePlayer caller = arg.Player();
if (caller != null && !permission.UserHasPermission(caller.UserIDString, PermAdmin) && !caller.IsAdmin)
{
arg.ReplyWith(Msg("NoPermission", caller.UserIDString));
return;
}
string value = arg.GetString(0, "toggle");
if (value == "on" || value == "off")
{
config.Enabled = value == "on";
SaveConfig();
string status = config.Enabled ? "on" : "off";
if (caller != null) SendReply(caller, Msg("SyncDefault", caller.UserIDString, status));
else Puts(string.Format("Server default is now {0}.", status));
}
ApplyToOnlineMembers();
}
private void ApplyToOnlineMembers()
{
int teams = 0;
foreach (BasePlayer player in BasePlayer.activePlayerList)
{
if (player == null || player.currentTeam == 0) continue;
RelationshipManager.PlayerTeam team = RelationshipManager.ServerInstance.FindTeam(player.currentTeam);
if (team == null) continue;
teams++;
if (WantsAutoAuth(player)) AuthorizePlayer(player, team);
else DeauthorizePlayer(player, team);
}
Puts(Msg("Synced", null, teams));
}
private void Unload()
{
SaveData();
}
}
}Hooks used: OnTeamAcceptInvite, OnTeamLeave, OnTeamKick, OnTeamDisbanded
Config file
Created automatically on first load at oxide/config/TeamAutoAuth.json.
{
"Enabled by default": true,
"Authorize tool cupboards": true,
"Authorize code locks": true,
"Authorize auto turrets": true,
"Player opt-outs": []
}Commands and permissions
| Command | What it does | Permission |
|---|---|---|
/teamauth <on|off|status> | Turns automatic team authorization on or off for yourself, or prints the current state. | teamautoauth.use |
teamauth.sync <on|off> | Sets the server default and reapplies authorization to every online team member. | teamautoauth.admin |
| Permission | Grants |
|---|---|
teamautoauth.use | Allows a player to toggle automatic team authorization for themselves with /teamauth. |
teamautoauth.admin | Allows an admin to change the server default and rescan teams with teamauth.sync. |
How the code works
Record teams that already got a sync
OnTeamAcceptInvitefires before the player is actually added toplayerTeam.members, so the new member is read from theplayerargument rather than from the team object. The plugin keeps aHashSet<ulong> syncedTeamsand addsplayerTeam.teamIDafter a successful authorize.OnTeamDisbandedremoves the ID again so a later team that reuses the ID starts clean.Read the decision from config and data
WantsAutoAuthcombinesconfig.Enabledwith the storedPlayer opt-outsset: when the server default is on, a player is auto-authorized unless their ID is in the set; when the default is off, only players who have opted in (/teamauth on, which removes them from the set) are handled. The set is loaded fromoxide/data/TeamAutoAuth.jsoninInitand written bySaveDataon every change and inUnload.Authorize teammates on accept
OnTeamAcceptInvitecallsAuthorizePlayer(player, playerTeam), which callsApplyToEntities(..., grant: true). A single pass overBaseNetworkable.serverEntitiescasts each entity toBaseEntity, skips any whoseOwnerIDis not inplayerTeam.members, and then checks the three families the config switches allow:BuildingPrivlidge,CodeLockandAutoTurret. Each add is guarded by anIsAuthedorContainscheck, so repeated calls never duplicate entries.Deauthorize on leave, kick and disband
OnTeamLeaveandOnTeamKickboth callDeauthorizePlayerwith the departing player: for a kick that is the kicked member, which is the second parameter, not the kicker.OnTeamDisbandedcopiesplayerTeam.membersinto a new list and deauthorizes every member still online throughBasePlayer.FindByID. Every hook method returnsnullso default Rust behavior is untouched. Owner IDs are compared before removal so nobody is stripped from an entity they placed themselves.Gate the chat command behind a permission
[ChatCommand("teamauth")]checkspermission.UserHasPermissionagainstteamautoauth.useand replies from the lang file. Valid arguments areon,offandstatus; anything else prints the usage string. The only characters the plugin prints come fromLoadDefaultMessages, so the whole command surface can be retranslated without touching code.Reapply the server default from console
[ConsoleCommand("teamauth.sync")]takesonoroffviaarg.GetString(0, ...), writesconfig.EnabledthroughSaveConfig, then callsApplyToOnlineMembers, which walksBasePlayer.activePlayerList, resolvesRelationshipManager.ServerInstance.FindTeam(player.currentTeam)and authorizes or deauthorizes depending onWantsAutoAuth. It is usable from RCON; therearg.Player()is null and the reply goes to the console log.
Install it on your server
Save TeamAutoAuth.cs
Keep the file name exactly as the class name. Oxide matches them.Upload it to the plugins folder
Oxide:oxide/plugins/. Carbon:carbon/plugins/. The server compiles and loads it within a few seconds, no restart needed.Edit the config if you want
On first load the default config is written tooxide/config/TeamAutoAuth.json(Carbon:carbon/configs/TeamAutoAuth.json). After editing, reload it:oxide.reload TeamAutoAuth c.reload TeamAutoAuth # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip:oxide.grant group default teamautoauth.use c.grant group default teamautoauth.use # Carbon
Variations
Each one opens the generator with a ready prompt you can tweak.
Share storage boxes too
Extends the scan to storage containers and vending machines so teammates can open shared boxes.
Generate this versionOnly sync recent joins
Restricts authorization to players who joined the team within the last N minutes.
Generate this versionTell players in chat
Sends a lang message when authorization is granted or revoked, so players understand why locks changed.
Generate this version
FAQ
Does this use the team system or the cupboard list?
Both. It reads membership from RelationshipManager.PlayerTeam.members, then writes to each entity's own authorization list. Because it only writes on membership events, an admin who removes someone from a cupboard with the hammer is not fighting a background loop; the next join or leave is what triggers the next write.
How do I install it on Oxide or Carbon?
Drop TeamAutoAuth.cs into oxide/plugins (Carbon: carbon/plugins) and let it compile. Grant teamautoauth.use to your default group and teamautoauth.admin to staff with oxide.grant. The config file is created on first load with the feature enabled, so no further setup is needed.
What happens when an owner leaves a team?
The owner is not deauthorized on entities they own, because OwnerID matches their userID. Teammates are still deauthorized from those entities, so leaving a group does not silently hand a base to the old crew. This behaviour is hard-coded and not exposed in the config.
Does it touch entities owned by players outside the team?
No. The scan checks entity.OwnerID against the team's current member list before calling any authorization method, so cupboards, code locks and turrets belonging to other players are never modified.
How do I turn it off for one player?
The player runs /teamauth off and their ID is stored in oxide/data; /teamauth on removes it. Removing the teamautoauth.use permission locks the setting. The server default is flipped with teamauth.sync off in the console or RCON, which also reapplies to online players.
Customize it for your server
Change the reward, add a cooldown, restrict it to VIPs. Start from this prompt and edit anything.
Edit the prompt on the next screen. 2 free plugins a month, no card.
More teams & clans plugins
- Simple clansAdds a lightweight clan system with /clan create, invite, join, leave and info, stores clans in a data file, prefixes members' chat with their tagโฆOnServerInitializedOnPlayerChatOnEntityTakeDamage
- Team death alertsSends a chat alert to every online teammate when a player dies, including the killer name and the map grid of the death.OnPlayerDeath
- Team info commandAdds a /team chat command that lists every member of the caller's Rust team with online status, health and map grid for online members.
Last updated Oct 26, 2026.
Customize team auto authorization
Customize