Add more RBAC

This commit is contained in:
Sambhav Saggi
2022-02-15 20:20:15 -05:00
parent fc22b72f43
commit 052e78b44d
3 changed files with 114 additions and 47 deletions
+13 -4
View File
@@ -12,6 +12,8 @@ There is NO admin configuration! You must use the API to configure the program!
**This is for Jellyfin 10.8**
**This README reflects the __main__ branch! Switch tags to view version-specific documentation!**
## Tested Providers
- Google OpenID: Works, but usernames are all numeric
@@ -53,7 +55,7 @@ Build the zipped plugin with `jprm --verbosity=debug plugin build .`.
Example for adding a SAML configuration with the API using [curl](https://curl.se/):
`curl -v -X POST -H "Content-Type: application/json" -d '{"samlEndpoint": "https://keycloak.example.com/realms/test/protocol/saml", "samlClientId": "jellyfin-saml", "samlCertificate": "Very long base64 encoded string here", "enabled": true, "enableAllFolders": true, "enabledFolders": ["folder1", "folder2"], "adminRoles": [], "roles": []}' "https://myjellyfin.example.com/sso/SAML/Add?api_key=API_KEY_HERE"`
`curl -v -X POST -H "Content-Type: application/json" -d '{"samlEndpoint": "https://keycloak.example.com/realms/test/protocol/saml", "samlClientId": "jellyfin-saml", "samlCertificate": "Very long base64 encoded string here", "enabled": true, "enableAuthorization": true, "enableAllFolders": true, "enabledFolders": ["folder1", "folder2"], "adminRoles": [], "roles": [], "enableFolderRoles": false, "folderRoleMapping": [{"role": "Movies", "folders": ["Movie1", "Movie2"]}]}' "https://myjellyfin.example.com/sso/SAML/Add?api_key=API_KEY_HERE"`
Make sure that the JSON is the same as the configuration you would like.
@@ -72,7 +74,7 @@ Make sure that `clientid` is replaced with the actual client ID!
Example for adding an OpenID configuration with the API using [curl](https://curl.se/)
`curl -v -X POST -H "Content-Type: application/json" -d '{"oidEndpoint": "https://keycloak.example.com/realms/test", "oidClientId": "jellyfin-oid", "oidSecret": "short secret here", "enabled": true, "enableAllFolders": true, "enabledFolders": ["folder3", "folder4"], "adminRoles": [], "roles": []}' "https://myjellyfin.example.com/sso/OID/Add?api_key=API_KEY_HERE"`
`curl -v -X POST -H "Content-Type: application/json" -d '{"oidEndpoint": "https://keycloak.example.com/realms/test", "oidClientId": "jellyfin-oid", "oidSecret": "short secret here", "enabled": true, "enableAllFolders": true, "enabledFolders": ["folder3", "folder4"], "enableAuthorization": true, "adminRoles": [], "roles": [], "enableFolderRoles": false, roleClaim: "realm_roles", "folderRoleMapping": [{"role": "Movies", "folders": ["Movie1", "Movie2"]}]}' "https://myjellyfin.example.com/sso/OID/Add?api_key=API_KEY_HERE"`
The OpenID provider must have the following configuration (again, I am using Keycloak)
@@ -110,10 +112,13 @@ These all require authorization. Append an API key to the end of the request: `c
- `samlClientId`: string. The SAML client ID.
- `samlCertificate`: string. The base64 encoded SAML certificate.
- `enabled`: boolean. Determines if the provider is enabled or not.
- `enableAuthorization`: boolean: Determines if the plugin sets permissions for the user. If false, the user will start with no permissions and an administrator will add permissions. The permissions of existing users will not be rewritten on subsequent logins.
- `enableAllFolders`: boolean. Determines if the client logging in is allowed access to all folders.
- `enabledFolders`: array of strings. If `enableAllFolders` is set to false, then this will be used to determine what folders the users who log in through this provider are allowed to use.
- `roles`: array of strings. This validates the SAML response against the `Role` attribute. If a user has any of these roles, then the user is authenticated. Leave blank to disable role checking.
- `adminRoles`: array of strings. This uses SAML response's `Role` attributes. If a user has any of these roles, then the user is an admin. Leave blank to disable (default is to not enable admin permissions).
- `enableFolderRoles`: boolean. Determines if role-based folder access should be used.
- `folderRoleMapping`: object in the format "role": string and "folders": array of strings. The user with this role will have access to the following folders if `enableFolderRoles` is enabled. To get the IDs of the folders, GET the `/Library/MediaFolders` URL with an API key. Look for the `Id` attribute.
- GET `SAML/Del/clientId`: This removes a configuration for SAML for a given client ID.
- GET `SAML/Get`: Lists the configurations currently available.
@@ -141,10 +146,14 @@ These all require authorization. Append an API key to the end of the request: `c
- `oidClientId`: string. The OpenID client ID.
- `oidSecret`: string. The OpenID secret.
- `enabled`: boolean. Determines if the provider is enabled or not.
- `enableAuthorization`: boolean: Determines if the plugin sets permissions for the user. If false, the user will start with no permissions and an administrator will add permissions. The permissions of existing users will not be rewritten on subsequent logins.
- `enableAllFolders`: boolean. Determines if the client logging in is allowed access to all folders.
- `enabledFolders`: array of strings. If `enableAllFolders` is set to false, then this will be used to determine what folders the users who log in through this provider are allowed to use.
- `roles`: array of strings. This validates the OpenID response against the `realm_access` claim. If a user has any of these roles, then the user is authenticated. Leave blank to disable role checking. This currently only works for Keycloak (to my knowledge).
- `adminRoles`: array of strings. This uses the OpenID response against the `realm_access` claim. If a user has any of these roles, then the user is an admin. Leave blank to disable (default is to not enable admin permissions).
- `roles`: array of strings. This validates the OpenID response against the claim set in `roleClaim`. If a user has any of these roles, then the user is authenticated. Leave blank to disable role checking. This currently only works for Keycloak (to my knowledge).
- `adminRoles`: array of strings. This uses the OpenID response against the claim set in `roleClaim`. If a user has any of these roles, then the user is an admin. Leave blank to disable (default is to not enable admin permissions).
- `enableFolderRoles`: boolean. Determines if role-based folder access should be used.
- `folderRoleMapping`: object in the format "role": string and "folders": array of strings. The user with this role will have access to the following folders if `enableFolderRoles` is enabled. To get the IDs of the folders, GET the `/Library/MediaFolders` URL with an API key. Look for the `Id` attribute.
- `roleClaim`: string. This is the value in the OpenID response to check for roles. For Keycloak, it is `realm_roles` by default.
- GET `OID/Del/clientId`: This removes a configuration for OpenID for a given client ID.
- GET `OID/Get`: Lists the configurations currently available.
- GET `OID/States`: Lists currently active OpenID flows in progress.
+76 -43
View File
@@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.Net.Mime;
using System.Threading.Tasks;
using IdentityModel.Client;
using IdentityModel.OidcClient;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
@@ -68,6 +67,13 @@ public class SSOController : ControllerBase
return Content("Something went wrong...", MediaTypeNames.Text.Plain);
}
if (!config.EnableFolderRoles)
{
StateManager[Request.Query["state"]].Folders = new List<string>(config.EnabledFolders);
} else {
StateManager[Request.Query["state"]].Folders = new List<string>();
}
foreach (var claim in result.User.Claims)
{
if (claim.Type == "preferred_username")
@@ -79,15 +85,16 @@ public class SSOController : ControllerBase
}
}
// Check if allowed to login based on realm roles
if (config.Roles.Length != 0)
// Role processing
if (claim.Type == config.RoleClaim)
{
if (claim.Type == "realm_access") // This is specific to Keycloak. Don't use roles without Keycloak, I guess
List<string> roles = JsonConvert.DeserializeObject<IDictionary<string, List<string>>>(claim.Value)["roles"]; // Might need error handling here
foreach (string role in roles)
{
List<string> roles = JsonConvert.DeserializeObject<IDictionary<string, List<string>>>(claim.Value)["roles"]; // Might need error handling here
foreach (string validRoles in config.Roles)
// Check if allowed to login based on roles
if (config.Roles.Length != 0)
{
foreach (string role in roles)
foreach (string validRoles in config.Roles)
{
if (role.Equals(validRoles))
{
@@ -95,17 +102,10 @@ public class SSOController : ControllerBase
}
}
}
}
}
// Check if admin
if (config.AdminRoles.Length != 0)
{
if (claim.Type == "realm_access") // This is specific to Keycloak. Don't use roles without Keycloak, I guess
{
List<string> roles = JsonConvert.DeserializeObject<IDictionary<string, List<string>>>(claim.Value)["roles"]; // Might need error handling here
foreach (string validAdminRoles in config.AdminRoles)
// Check if admin based on roles
if (config.AdminRoles.Length != 0)
{
foreach (string role in roles)
foreach (string validAdminRoles in config.AdminRoles)
{
if (role.Equals(validAdminRoles))
{
@@ -113,6 +113,17 @@ public class SSOController : ControllerBase
}
}
}
// Get allowed folders from roles
if (config.EnableFolderRoles)
{
foreach (FolderRoleMap folderRoleMap in config.FolderRoleMapping)
{
if (role.Equals(folderRoleMap.Role))
{
StateManager[Request.Query["state"]].Folders.AddRange(folderRoleMap.Folders);
}
}
}
}
}
}
@@ -175,18 +186,18 @@ public class SSOController : ControllerBase
[Authorize(Policy = "RequiresElevation")]
[HttpPost("OID/Add")]
public void OIDAdd([FromBody] OIDConfig oidConfig)
public void OIDAdd([FromBody] OIDConfig config)
{
var configuration = SSOPlugin.Instance.Configuration;
for (var i = 0; i < configuration.OIDConfigs.Count; i++)
{
if (configuration.OIDConfigs[i].OIDClientId.Equals(oidConfig.OIDClientId))
if (configuration.OIDConfigs[i].OIDClientId.Equals(config.OIDClientId))
{
configuration.OIDConfigs.RemoveAt(i);
}
}
configuration.OIDConfigs.Add(oidConfig);
configuration.OIDConfigs.Add(config);
SSOPlugin.Instance.UpdateConfiguration(configuration);
}
@@ -225,15 +236,15 @@ public class SSOController : ControllerBase
[Produces(MediaTypeNames.Application.Json)]
public async Task<ActionResult> OIDAuth([FromBody] AuthResponse response)
{
foreach (var oidConfig in SSOPlugin.Instance.Configuration.OIDConfigs)
foreach (var config in SSOPlugin.Instance.Configuration.OIDConfigs)
{
if (oidConfig.OIDClientId == response.Provider && oidConfig.Enabled)
if (config.OIDClientId == response.Provider && config.Enabled)
{
foreach (var kvp in StateManager)
{
if (kvp.Value.State.State.Equals(response.Data) && kvp.Value.Valid)
{
var authenticationResult = await Authenticate(kvp.Value.Username, kvp.Value.Admin, oidConfig.EnableAllFolders, oidConfig.EnabledFolders, response)
var authenticationResult = await Authenticate(kvp.Value.Username, kvp.Value.Admin, config.EnableAuthorization, config.EnableAllFolders, kvp.Value.Folders.ToArray(), response)
.ConfigureAwait(false);
return Ok(authenticationResult);
}
@@ -248,18 +259,20 @@ public class SSOController : ControllerBase
public ActionResult SAMLPost(string provider)
{
// I'm sure there's a better way than using nested for loops but eh whatever
foreach (var samlConfig in SSOPlugin.Instance.Configuration.SamlConfigs)
foreach (var config in SSOPlugin.Instance.Configuration.SamlConfigs)
{
if (samlConfig.SamlClientId == provider && samlConfig.Enabled)
if (config.SamlClientId == provider && config.Enabled)
{
var samlResponse = new Response(samlConfig.SamlCertificate, Request.Form["SAMLResponse"]);
if (samlConfig.Roles.Length == 0)
var samlResponse = new Response(config.SamlCertificate, Request.Form["SAMLResponse"]);
// If no roles are configured, don't use RBAC
if (config.Roles.Length == 0)
{
return Content(WebResponse.SamlGenerator(xml: Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(samlResponse.Xml)), provider: provider, baseUrl: GetRequestBase()), MediaTypeNames.Text.Html);
}
// Check if user is allowed to log in based on roles
foreach (string role in samlResponse.GetCustomAttributes("Role"))
{
foreach (string allowedRole in samlConfig.Roles)
foreach (string allowedRole in config.Roles)
{
if (allowedRole.Equals(role))
{
@@ -267,7 +280,7 @@ public class SSOController : ControllerBase
}
}
}
return Content("401 Forbidden");
return Content("401 Forbidden"); // TODO: Return error code as well
}
}
@@ -294,18 +307,18 @@ public class SSOController : ControllerBase
[Authorize(Policy = "RequiresElevation")]
[HttpPost("SAML/Add")]
public void SamlAdd([FromBody] SamlConfig samlConfig)
public void SamlAdd([FromBody] SamlConfig config)
{
var configuration = SSOPlugin.Instance.Configuration;
for (var i = 0; i < configuration.SamlConfigs.Count; i++)
{
if (configuration.SamlConfigs[i].SamlClientId.Equals(samlConfig.SamlClientId))
if (configuration.SamlConfigs[i].SamlClientId.Equals(config.SamlClientId))
{
configuration.SamlConfigs.RemoveAt(i);
}
}
configuration.SamlConfigs.Add(samlConfig);
configuration.SamlConfigs.Add(config);
SSOPlugin.Instance.UpdateConfiguration(configuration);
}
@@ -337,23 +350,39 @@ public class SSOController : ControllerBase
[Produces(MediaTypeNames.Application.Json)]
public async Task<ActionResult> SamlAuth([FromBody] AuthResponse response)
{
foreach (var samlConfig in SSOPlugin.Instance.Configuration.SamlConfigs)
foreach (var config in SSOPlugin.Instance.Configuration.SamlConfigs)
{
if (samlConfig.SamlClientId == response.Provider && samlConfig.Enabled)
if (config.SamlClientId == response.Provider && config.Enabled)
{
bool isAdmin = false;
var samlResponse = new Response(samlConfig.SamlCertificate, response.Data);
var samlResponse = new Response(config.SamlCertificate, response.Data);
List<string> folders;
if (!config.EnableFolderRoles)
{
folders = new List<string>(config.EnabledFolders);
} else {
folders = new List<string>();
}
foreach (string role in samlResponse.GetCustomAttributes("Role"))
{
foreach (string allowedRole in samlConfig.AdminRoles)
foreach (string allowedRole in config.AdminRoles)
{
if (allowedRole.Equals(role))
{
isAdmin = true;
}
}
if (config.EnableFolderRoles) {
foreach (FolderRoleMap folderRoleMap in config.FolderRoleMapping)
{
if (folderRoleMap.Role.Equals(role)) {
folders.AddRange(folderRoleMap.Folders);
}
}
}
}
var authenticationResult = await Authenticate(samlResponse.GetNameID(), isAdmin, samlConfig.EnableAllFolders, samlConfig.EnabledFolders, response)
var authenticationResult = await Authenticate(samlResponse.GetNameID(), isAdmin, config.EnableAuthorization, config.EnableAllFolders, folders.ToArray(), response)
.ConfigureAwait(false);
return Ok(authenticationResult);
}
@@ -362,7 +391,7 @@ public class SSOController : ControllerBase
return Problem("Something went wrong");
}
private async Task<AuthenticationResult> Authenticate(string username, bool isAdmin, bool enableAllFolders, string[] enabledFolders, AuthResponse authResponse)
private async Task<AuthenticationResult> Authenticate(string username, bool isAdmin, bool enableAuthorization, bool enableAllFolders, string[] enabledFolders, AuthResponse authResponse)
{
User user = null;
user = _userManager.GetUserByName(username);
@@ -373,11 +402,13 @@ public class SSOController : ControllerBase
user = await _userManager.CreateUserAsync(username).ConfigureAwait(false);
}
user.AuthenticationProviderId = GetType().FullName;
user.SetPermission(PermissionKind.IsAdministrator, isAdmin);
user.SetPermission(PermissionKind.EnableAllFolders, enableAllFolders);
if (!enableAllFolders)
{
user.SetPreference(PreferenceKind.EnabledFolders, enabledFolders);
if (enableAuthorization) {
user.SetPermission(PermissionKind.IsAdministrator, isAdmin);
user.SetPermission(PermissionKind.EnableAllFolders, enableAllFolders);
if (!enableAllFolders)
{
user.SetPreference(PreferenceKind.EnabledFolders, enabledFolders);
}
}
await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
@@ -447,4 +478,6 @@ public class TimedAuthorizeState
public bool Admin { get; set; }
public string Email { get; set; }
public List<string> Folders { get; set; }
}
+25
View File
@@ -37,6 +37,8 @@ public class SamlConfig
public bool Enabled { get; set; }
public bool EnableAuthorization { get; set; }
public bool EnableAllFolders { get; set; }
public string[] EnabledFolders { get; set; }
@@ -44,6 +46,12 @@ public class SamlConfig
public string[] AdminRoles { get; set; }
public string[] Roles { get; set; }
public bool EnableFolderRoles { get; set; }
[XmlArray("FolderRoleMappings")]
[XmlArrayItem(typeof(FolderRoleMap), ElementName = "FolderRoleMappings")]
public List<FolderRoleMap> FolderRoleMapping { get; set; }
}
[XmlRoot("PluginConfiguration")]
@@ -57,6 +65,8 @@ public class OIDConfig
public bool Enabled { get; set; }
public bool EnableAuthorization { get; set; }
public bool EnableAllFolders { get; set; }
public string[] EnabledFolders { get; set; }
@@ -64,4 +74,19 @@ public class OIDConfig
public string[] AdminRoles { get; set; }
public string[] Roles { get; set; }
public bool EnableFolderRoles { get; set; }
[XmlArray("FolderRoleMappings")]
[XmlArrayItem(typeof(FolderRoleMap), ElementName = "FolderRoleMappings")]
public List<FolderRoleMap> FolderRoleMapping { get; set; }
public string RoleClaim { get; set; }
}
public class FolderRoleMap
{
public string Role { get; set; }
public List<string> Folders { get; set; }
}