Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
439 changes: 439 additions & 0 deletions src/dotnet/APIView/APIViewUnitTests/PermissionsControllerTests.cs

Large diffs are not rendered by default.

470 changes: 470 additions & 0 deletions src/dotnet/APIView/APIViewUnitTests/PermissionsManagerTests.cs

Large diffs are not rendered by default.

32 changes: 32 additions & 0 deletions src/dotnet/APIView/APIViewWeb/Client/src/shared/layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,38 @@ document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('[data-bs-toggle="popover"]')
.forEach(el => new bootstrap.Popover(el));

// Check if user is admin and show admin settings link
const adminSettingsItem = document.querySelector('.admin-settings-item');
if (adminSettingsItem) {
fetch('/api/Permissions/me', { credentials: 'include' })
.then(response => {
if (response.ok) {
return response.json();
}
throw new Error('Failed to fetch permissions');
})
.then(permissions => {
const isAdmin = permissions?.roles?.some(
(role: { kind: string; role: string }) => role.kind === 'global' && role.role.toLowerCase() === 'admin'
);
if (isAdmin) {
adminSettingsItem.classList.remove('d-none');
const adminLink = adminSettingsItem.querySelector('a');
if (adminLink) {
const host = window.location.hostname;
let spaBaseUrl: string;
if (host === 'localhost' || host === '127.0.0.1') {
spaBaseUrl = 'https://localhost:4200';
} else {
spaBaseUrl = `${window.location.protocol}//spa.${host}`;
}
adminLink.setAttribute('href', `${spaBaseUrl}/admin/permissions`);
}
}
})
.catch(err => console.error('Failed to check admin status:', err));
}

// Theme switcher
const validThemes = ['light-theme', 'dark-theme', 'dark-solarized-theme'];

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using APIViewWeb.Helpers;
using APIViewWeb.LeanModels;
using APIViewWeb.Managers.Interfaces;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace APIViewWeb.LeanControllers;

public class PermissionsController : BaseApiController
{
private readonly IPermissionsManager _permissionsManager;

public PermissionsController(IPermissionsManager permissionsManager)
{
_permissionsManager = permissionsManager;
}

/// <summary>
/// Get current user's effective permissions
/// </summary>
[HttpGet("me")]
public async Task<ActionResult<EffectivePermissions>> GetMyPermissions()
{
var userName = User.GetGitHubLogin();
var permissions = await _permissionsManager.GetEffectivePermissionsAsync(userName);
return new LeanJsonResult(permissions, StatusCodes.Status200OK);
}

/// <summary>
/// Get all usernames for autocomplete (Admin only)
/// </summary>
[HttpGet("users")]
public async Task<ActionResult<IEnumerable<string>>> GetAllUsernames()
{
var userName = User.GetGitHubLogin();
if (!await _permissionsManager.IsAdminAsync(userName))
{
return Forbid();
}

IEnumerable<string> users = await _permissionsManager.GetAllUsernamesAsync();
return new LeanJsonResult(users, StatusCodes.Status200OK);
}

/// <summary>
/// Get all permission groups (Admin only)
/// </summary>
[HttpGet("groups")]
public async Task<ActionResult> GetAllGroups()
{
var userName = User.GetGitHubLogin();
var isAdmin = await _permissionsManager.IsAdminAsync(userName);
if (!isAdmin)
{
return Forbid();
}

var groups = await _permissionsManager.GetAllGroupsAsync();
return new LeanJsonResult(groups, StatusCodes.Status200OK);
}

/// <summary>
/// Get a specific group by ID (Admin only)
/// </summary>
[HttpGet("groups/{groupId}")]
public async Task<ActionResult<GroupPermissionsModel>> GetGroup(string groupId)
{
var userName = User.GetGitHubLogin();
if (!await _permissionsManager.IsAdminAsync(userName))
{
return Forbid();
}

GroupPermissionsModel group = await _permissionsManager.GetGroupAsync(groupId);
if (group == null)
{
return NotFound($"Group with ID '{groupId}' not found.");
}

return new LeanJsonResult(group, StatusCodes.Status200OK);
}

/// <summary>
/// Create a new permission group (Admin only)
/// </summary>
[HttpPost("groups")]
public async Task<ActionResult<GroupPermissionsModel>> CreateGroup([FromBody] GroupPermissionsRequest request)
{
var userName = User.GetGitHubLogin();
if (!await _permissionsManager.IsAdminAsync(userName))
{
return Forbid();
}

if (string.IsNullOrWhiteSpace(request.GroupId))
{
return BadRequest("GroupId is required.");
}

if (string.IsNullOrWhiteSpace(request.GroupName))
{
return BadRequest("GroupName is required.");
}

if (request.Roles != null)
{
foreach (var role in request.Roles)
{
if (role is LanguageScopedRoleAssignment scopedRole &&
string.IsNullOrWhiteSpace(scopedRole.Language))
{
return BadRequest("Language is required for language-scoped role assignments.");
}
}
}

GroupPermissionsModel existingGroup = await _permissionsManager.GetGroupAsync(request.GroupId);
if (existingGroup != null)
{
return Conflict($"Group with ID '{request.GroupId}' already exists.");
}

GroupPermissionsModel group = await _permissionsManager.CreateGroupAsync(request, userName);
return new LeanJsonResult(group, StatusCodes.Status201Created);
}

/// <summary>
/// Update an existing permission group (Admin only)
/// </summary>
[HttpPut("groups/{groupId}")]
public async Task<ActionResult<GroupPermissionsModel>> UpdateGroup(string groupId,
[FromBody] GroupPermissionsRequest request)
{
var userName = User.GetGitHubLogin();
if (!await _permissionsManager.IsAdminAsync(userName))
{
return Forbid();
}

if (request.Roles != null)
{
foreach (var role in request.Roles)
{
if (role is LanguageScopedRoleAssignment scopedRole &&
string.IsNullOrWhiteSpace(scopedRole.Language))
{
return BadRequest("Language is required for language-scoped role assignments.");
}
}
}

try
{
GroupPermissionsModel group = await _permissionsManager.UpdateGroupAsync(groupId, request, userName);
return new LeanJsonResult(group, StatusCodes.Status200OK);
}
catch (ArgumentException ex)
{
return NotFound(ex.Message);
}
}

/// <summary>
/// Delete a permission group (Admin only)
/// </summary>
[HttpDelete("groups/{groupId}")]
public async Task<ActionResult> DeleteGroup(string groupId)
{
var userName = User.GetGitHubLogin();
if (!await _permissionsManager.IsAdminAsync(userName))
{
return Forbid();
}

await _permissionsManager.DeleteGroupAsync(groupId);
return NoContent();
}

/// <summary>
/// Add members to a group (Admin only)
/// </summary>
[HttpPost("groups/{groupId}/members")]
public async Task<ActionResult<AddMembersResult>> AddMembers(string groupId, [FromBody] AddMembersRequest request)
{
var userName = User.GetGitHubLogin();
if (!await _permissionsManager.IsAdminAsync(userName))
{
return Forbid();
}

if (request.UserIds == null || request.UserIds.Count == 0)
{
return BadRequest("At least one userId is required.");
}

try
{
AddMembersResult result = await _permissionsManager.AddMembersToGroupAsync(groupId, request.UserIds, userName);

if (result.InvalidUsers.Count > 0 && result.AddedUsers.Count == 0)
{
// All users were invalid
return BadRequest(new {
message = "None of the specified users exist in our database.",
invalidUsers = result.InvalidUsers
});
}

return new LeanJsonResult(result, StatusCodes.Status200OK);
}
catch (ArgumentException ex)
{
return NotFound(ex.Message);
}
}

/// <summary>
/// Remove a member from a group (Admin only)
/// </summary>
[HttpDelete("groups/{groupId}/members/{userId}")]
public async Task<ActionResult> RemoveMember(string groupId, string userId)
{
var userName = User.GetGitHubLogin();
if (!await _permissionsManager.IsAdminAsync(userName))
{
return Forbid();
}

try
{
await _permissionsManager.RemoveMemberFromGroupAsync(groupId, userId, userName);
return NoContent();
}
catch (ArgumentException ex)
{
return NotFound(ex.Message);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Threading.Tasks;
using APIViewWeb.Helpers;
using APIViewWeb.Managers.Interfaces;
using APIViewWeb.Models;
using APIViewWeb.Repositories;
using Microsoft.AspNetCore.Http;
Expand All @@ -10,10 +11,12 @@ namespace APIViewWeb.LeanControllers
public class UserProfileController : BaseApiController
{
private readonly UserProfileCache _userProfileCache;
private readonly IPermissionsManager _permissionsManager;

public UserProfileController(UserProfileCache userProfileCache)
public UserProfileController(UserProfileCache userProfileCache, IPermissionsManager permissionsManager)
{
_userProfileCache = userProfileCache;
_permissionsManager = permissionsManager;
}

[HttpGet]
Expand All @@ -23,6 +26,8 @@ public async Task<ActionResult<UserProfileModel>> GetUserPreference([FromQuery]s
try
{
var userProfile = await _userProfileCache.GetUserProfileAsync(userName, createIfNotExist: false);
userProfile.Permissions = await _permissionsManager.GetEffectivePermissionsAsync(userName);

return new LeanJsonResult(userProfile, StatusCodes.Status200OK);
}
catch
Expand Down
Loading