tgstation-server 6.8.0
The /tg/station 13 server suite
Loading...
Searching...
No Matches
ApiRootController.cs
Go to the documentation of this file.
1using System;
2using System.Linq;
3using System.Threading;
4using System.Threading.Tasks;
5
6using Microsoft.AspNetCore.Authorization;
7using Microsoft.AspNetCore.Mvc;
8using Microsoft.EntityFrameworkCore;
9using Microsoft.Extensions.Logging;
10using Microsoft.Extensions.Options;
11using Microsoft.Extensions.Primitives;
12using Microsoft.Net.Http.Headers;
13
14using Octokit;
15
29
31{
35 [Route(Routes.ApiRoot)]
36 public sealed class ApiRootController : ApiController
37 {
42
47
52
57
62
67
72
77
82
87
106 IDatabaseContext databaseContext,
107 IAuthenticationContext authenticationContext,
117 IOptions<GeneralConfiguration> generalConfigurationOptions,
118 ILogger<ApiRootController> logger,
119 IApiHeadersProvider apiHeadersProvider)
120 : base(
121 databaseContext,
122 authenticationContext,
123 apiHeadersProvider,
124 logger,
125 false)
126 {
127 this.tokenFactory = tokenFactory ?? throw new ArgumentNullException(nameof(tokenFactory));
128 this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory));
129 this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
130 this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
131 this.identityCache = identityCache ?? throw new ArgumentNullException(nameof(identityCache));
132 this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
133 this.oAuthProviders = oAuthProviders ?? throw new ArgumentNullException(nameof(oAuthProviders));
134 this.swarmService = swarmService ?? throw new ArgumentNullException(nameof(swarmService));
135 this.serverControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl));
136 generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
137 }
138
146 [HttpGet]
147 [AllowAnonymous]
148 [ProducesResponseType(typeof(ServerInformationResponse), 200)]
149 public IActionResult ServerInfo()
150 {
151 // if they tried to authenticate in any form and failed, let them know immediately
152 bool failIfUnauthed;
153 if (ApiHeaders == null)
154 {
155 try
156 {
157 // we only allow authorization header issues
159 }
160 catch (HeadersException ex)
161 {
162 return HeadersIssue(ex);
163 }
164
165 failIfUnauthed = Request.Headers.Authorization.Count > 0;
166 }
167 else
168 failIfUnauthed = ApiHeaders.Token != null;
169
170 if (failIfUnauthed && !AuthenticationContext.Valid)
171 return Unauthorized();
172
173 return Json(new ServerInformationResponse
174 {
176 ApiVersion = ApiHeaders.Version,
177 DMApiVersion = DMApiConstants.InteropVersion,
178 MinimumPasswordLength = generalConfiguration.MinimumPasswordLength,
179 InstanceLimit = generalConfiguration.InstanceLimit,
181 UserGroupLimit = generalConfiguration.UserGroupLimit,
182 ValidInstancePaths = generalConfiguration.ValidInstancePaths,
183 WindowsHost = platformIdentifier.IsWindows,
184 SwarmServers = swarmService.GetSwarmServers(),
185 OAuthProviderInfos = oAuthProviders.ProviderInfos(),
186 UpdateInProgress = serverControl.UpdateInProgress,
187 });
188 }
189
199 [HttpPost]
200 [ProducesResponseType(typeof(TokenResponse), 200)]
201 [ProducesResponseType(typeof(ErrorMessageResponse), 429)]
202#pragma warning disable CA1506 // TODO: Decomplexify
203 public async ValueTask<IActionResult> CreateToken(CancellationToken cancellationToken)
204 {
205 if (ApiHeaders == null)
206 {
207 Response.Headers.Add(HeaderNames.WWWAuthenticate, new StringValues($"basic realm=\"Create TGS {ApiHeaders.BearerAuthenticationScheme} token\""));
209 }
210
212 return BadRequest(new ErrorMessageResponse(ErrorCode.TokenWithToken));
213
214 var oAuthLogin = ApiHeaders.OAuthProvider.HasValue;
215
216 ISystemIdentity? systemIdentity = null;
217 if (!oAuthLogin)
218 try
219 {
220 // trust the system over the database because a user's name can change while still having the same SID
221 systemIdentity = await systemIdentityFactory.CreateSystemIdentity(ApiHeaders.Username!, ApiHeaders.Password!, cancellationToken);
222 }
223 catch (NotImplementedException)
224 {
225 // Intentionally suppressed
226 }
227
228 using (systemIdentity)
229 {
230 // Get the user from the database
231 IQueryable<Models.User> query = DatabaseContext.Users.AsQueryable();
232 if (oAuthLogin)
233 {
234 var oAuthProvider = ApiHeaders.OAuthProvider!.Value;
235 string? externalUserId;
236 try
237 {
238 var validator = oAuthProviders
239 .GetValidator(oAuthProvider);
240
241 if (validator == null)
242 return BadRequest(new ErrorMessageResponse(ErrorCode.OAuthProviderDisabled));
243
244 externalUserId = await validator
245 .ValidateResponseCode(ApiHeaders.OAuthCode!, cancellationToken);
246
247 Logger.LogTrace("External {oAuthProvider} UID: {externalUserId}", oAuthProvider, externalUserId);
248 }
249 catch (RateLimitExceededException ex)
250 {
251 return RateLimit(ex);
252 }
253
254 if (externalUserId == null)
255 return Unauthorized();
256
257 query = query.Where(
258 x => x.OAuthConnections!.Any(
259 y => y.Provider == oAuthProvider
260 && y.ExternalUserId == externalUserId));
261 }
262 else
263 {
264 var canonicalUserName = Models.User.CanonicalizeName(ApiHeaders.Username!);
265 if (canonicalUserName == Models.User.CanonicalizeName(Models.User.TgsSystemUserName))
266 return Unauthorized();
267
268 if (systemIdentity == null)
269 query = query.Where(x => x.CanonicalName == canonicalUserName);
270 else
271 query = query.Where(x => x.CanonicalName == canonicalUserName || x.SystemIdentifier == systemIdentity.Uid);
272 }
273
274 var users = await query
275 .Select(x => new Models.User
276 {
277 Id = x.Id,
278 PasswordHash = x.PasswordHash,
279 Enabled = x.Enabled,
280 Name = x.Name,
281 SystemIdentifier = x.SystemIdentifier,
282 })
283 .ToListAsync(cancellationToken);
284
285 // Pick the DB user first
286 var user = users
287 .OrderByDescending(dbUser => dbUser.SystemIdentifier == null)
288 .FirstOrDefault();
289
290 // No user? You're not allowed
291 if (user == null)
292 return Unauthorized();
293
294 // A system user may have had their name AND password changed to one in our DB...
295 // Or a DB user was created that had the same user/pass as a system user
296 // Dumb admins...
297 // FALLBACK TO THE DB USER HERE, DO NOT REVEAL A SYSTEM LOGIN!!!
298 // This of course, allows system users to discover TGS users in this (HIGHLY IMPROBABLE) case but that is not our fault
299 var originalHash = user.PasswordHash;
300 var isLikelyDbUser = originalHash != null;
301 bool usingSystemIdentity = systemIdentity != null && !isLikelyDbUser;
302 if (!oAuthLogin)
303 if (!usingSystemIdentity)
304 {
305 // DB User password check and update
306 if (!isLikelyDbUser || !cryptographySuite.CheckUserPassword(user, ApiHeaders.Password!))
307 return Unauthorized();
308 if (user.PasswordHash != originalHash)
309 {
310 Logger.LogDebug("User ID {userId}'s password hash needs a refresh, updating database.", user.Id);
311 var updatedUser = new Models.User
312 {
313 Id = user.Id,
314 };
315 DatabaseContext.Users.Attach(updatedUser);
316 updatedUser.PasswordHash = user.PasswordHash;
317 await DatabaseContext.Save(cancellationToken);
318 }
319 }
320 else
321 {
322 var usernameMismatch = systemIdentity!.Username != user.Name;
323 if (isLikelyDbUser || usernameMismatch)
324 {
325 DatabaseContext.Users.Attach(user);
326 if (isLikelyDbUser)
327 {
328 // cleanup from https://github.com/tgstation/tgstation-server/issues/1528
329 Logger.LogDebug("System user ID {userId}'s PasswordHash is polluted, updating database.", user.Id);
330 user.PasswordHash = null;
331 user.LastPasswordUpdate = DateTimeOffset.UtcNow;
332 }
333
334 if (usernameMismatch)
335 {
336 // System identity username change update
337 Logger.LogDebug("User ID {userId}'s system identity needs a refresh, updating database.", user.Id);
338 user.Name = systemIdentity.Username;
339 user.CanonicalName = Models.User.CanonicalizeName(user.Name);
340 }
341
342 await DatabaseContext.Save(cancellationToken);
343 }
344 }
345
346 // Now that the bookeeping is done, tell them to fuck off if necessary
347 if (!user.Enabled!.Value)
348 {
349 Logger.LogTrace("Not logging in disabled user {userId}.", user.Id);
350 return Forbid();
351 }
352
353 var token = tokenFactory.CreateToken(user, oAuthLogin);
354 if (usingSystemIdentity)
355 {
356 // expire the identity slightly after the auth token in case of lag
357 var identExpiry = token.ParseJwt().ValidTo;
358 identExpiry += tokenFactory.ValidationParameters.ClockSkew;
359 identExpiry += TimeSpan.FromSeconds(15);
360 await identityCache.CacheSystemIdentity(user, systemIdentity!, identExpiry);
361 }
362
363 Logger.LogDebug("Successfully logged in user {userId}!", user.Id);
364
365 return Json(token);
366 }
367 }
368#pragma warning restore CA1506
369 }
370}
Represents the header that must be present for every server request.
Definition: ApiHeaders.cs:25
bool IsTokenAuthentication
If the header uses OAuth or TGS JWT authentication.
Definition: ApiHeaders.cs:129
string? Username
The client's username.
Definition: ApiHeaders.cs:109
static readonly Version Version
Get the version of the Api the caller is using.
Definition: ApiHeaders.cs:69
string? OAuthCode
The OAuth code in use.
Definition: ApiHeaders.cs:119
OAuthProvider? OAuthProvider
The Models.OAuthProvider the Token is for, if any.
Definition: ApiHeaders.cs:124
string? Password
The client's password.
Definition: ApiHeaders.cs:114
Thrown when trying to generate ApiHeaders from Microsoft.AspNetCore.Http.Headers.RequestHeaders fails...
virtual ? long Id
The ID of the entity.
Definition: EntityId.cs:13
uint UserGroupLimit
The maximum number of user groups allowed.
uint MinimumPasswordLength
Minimum length of database user passwords.
ICollection< string >? ValidInstancePaths
Limits the locations instances may be created or attached from.
uint InstanceLimit
The maximum number of Instances allowed.
Represents an error message returned by the server.
Represents a JWT returned by the API.
Definition: TokenResponse.cs:9
JsonWebToken ParseJwt()
Parses the Bearer as a JsonWebToken.
Routes to a server actions.
Definition: Routes.cs:9
const string ApiRoot
The root of API methods.
Definition: Routes.cs:13
Constants used for communication with the DMAPI.
static readonly Version InteropVersion
The DMAPI InteropVersion being used.
Base Controller for API functions.
IActionResult HeadersIssue(HeadersException headersException)
Response for missing/Invalid headers.
new ObjectResult Unauthorized()
Generic 401 response.
ObjectResult RateLimit(RateLimitExceededException rateLimitException)
429 response for a given rateLimitException .
ILogger< ApiController > Logger
The ILogger for the ApiController.
Root ApiController for the Application.
ApiRootController(IDatabaseContext databaseContext, IAuthenticationContext authenticationContext, ITokenFactory tokenFactory, ISystemIdentityFactory systemIdentityFactory, ICryptographySuite cryptographySuite, IAssemblyInformationProvider assemblyInformationProvider, IIdentityCache identityCache, IOAuthProviders oAuthProviders, IPlatformIdentifier platformIdentifier, ISwarmService swarmService, IServerControl serverControl, IOptions< GeneralConfiguration > generalConfigurationOptions, ILogger< ApiRootController > logger, IApiHeadersProvider apiHeadersProvider)
Initializes a new instance of the ApiRootController class.
readonly ITokenFactory tokenFactory
The ITokenFactory for the ApiRootController.
readonly ISystemIdentityFactory systemIdentityFactory
The ISystemIdentityFactory for the ApiRootController.
readonly IServerControl serverControl
The IServerControl for the ApiRootController.
readonly IIdentityCache identityCache
The IIdentityCache for the ApiRootController.
readonly ICryptographySuite cryptographySuite
The ICryptographySuite for the ApiRootController.
async ValueTask< IActionResult > CreateToken(CancellationToken cancellationToken)
Attempt to authenticate a User using ApiController.ApiHeaders.
readonly IPlatformIdentifier platformIdentifier
The IPlatformIdentifier for the ApiRootController.
readonly GeneralConfiguration generalConfiguration
The GeneralConfiguration for the ApiRootController.
IActionResult ServerInfo()
Main page of the Application.
readonly IAssemblyInformationProvider assemblyInformationProvider
The IAssemblyInformationProvider for the ApiRootController.
readonly IOAuthProviders oAuthProviders
The IOAuthProviders for the ApiRootController.
readonly ISwarmService swarmService
The ISwarmService for the ApiRootController.
Backend abstract implementation of IDatabaseContext.
Task Save(CancellationToken cancellationToken)
Saves changes made to the IDatabaseContext. A Task representing the running operation.
DbSet< User > Users
The Users in the DatabaseContext.
bool Valid
If the IAuthenticationContext is for a valid login.
ApiHeaders CreateAuthlessHeaders()
Attempt to create Api.ApiHeaders without checking for the presence of an Microsoft....
HeadersException? HeadersException
The Api.HeadersException thrown when attempting to parse the ApiHeaders if any.
Represents a service that may take an updated Host assembly and run it, stopping the current assembly...
bool UpdateInProgress
Whether or not the server is currently updating.
Represents the currently authenticated Models.User.
Contains various cryptographic functions.
bool CheckUserPassword(User user, string password)
Checks a given password matches a given user 's User.PasswordHash. This may result in User....
ValueTask CacheSystemIdentity(User user, ISystemIdentity systemIdentity, DateTimeOffset expiry)
Keep a user 's systemIdentity alive until an expiry time.
Task< ISystemIdentity?> CreateSystemIdentity(User user, CancellationToken cancellationToken)
Create a ISystemIdentity for a given user .
Represents a user on the current global::System.Runtime.InteropServices.OSPlatform.
string Uid
A unique identifier for the user.
TokenResponse CreateToken(Models.User user, bool oAuth)
Create a TokenResponse for a given user .
TokenValidationParameters ValidationParameters
The TokenValidationParameters for the ITokenFactory.
IOAuthValidator? GetValidator(OAuthProvider oAuthProvider)
Gets the IOAuthValidator for a given oAuthProvider .
Dictionary< OAuthProvider, OAuthProviderInfo > ProviderInfos()
Gets a Dictionary<TKey, TValue> of the provider client IDs.
Used for swarm operations. Functions may be no-op based on configuration.
List< SwarmServerResponse >? GetSwarmServers()
Gets the list of SwarmServerResponses in the swarm, including the current one.
For identifying the current platform.
bool IsWindows
If the current platform is a Windows platform.
ErrorCode
Types of Response.ErrorMessageResponses that the API may return.
Definition: ErrorCode.cs:12