mirror of
https://github.com/duplicati/duplicati.git
synced 2025-11-28 11:30:24 +08:00
This PR adds the lookup data to the module output, such that the FE does not need to call a webmodule to get the dictionaries with lookup values.
228 lines
9.1 KiB
C#
228 lines
9.1 KiB
C#
// Copyright (C) 2025, The Duplicati Team
|
|
// https://duplicati.com, hello@duplicati.com
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a
|
|
// copy of this software and associated documentation files (the "Software"),
|
|
// to deal in the Software without restriction, including without limitation
|
|
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
// and/or sell copies of the Software, and to permit persons to whom the
|
|
// Software is furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in
|
|
// all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
// DEALINGS IN THE SOFTWARE.
|
|
|
|
using Duplicati.Library.Interface;
|
|
using Amazon.IdentityManagement;
|
|
using Amazon.IdentityManagement.Model;
|
|
using Duplicati.Library.Utility;
|
|
|
|
namespace Duplicati.Library.Backend
|
|
{
|
|
public class S3IAM : IWebModule
|
|
{
|
|
private const string KEY_OPERATION = "s3-operation";
|
|
private const string KEY_USERNAME = "s3-username";
|
|
private const string KEY_PASSWORD = "s3-password";
|
|
private const string KEY_PATH = "s3-path";
|
|
|
|
public enum Operation
|
|
{
|
|
CanCreateUser,
|
|
CreateIAMUser,
|
|
GetPolicyDoc
|
|
}
|
|
|
|
public const string POLICY_DOCUMENT_TEMPLATE =
|
|
@"
|
|
{
|
|
""Version"": ""2012-10-17"",
|
|
""Statement"": [
|
|
{
|
|
""Sid"": ""Stmt1390497858034"",
|
|
""Effect"": ""Allow"",
|
|
""Action"": [
|
|
""s3:GetObject"",
|
|
""s3:PutObject"",
|
|
""s3:ListBucket"",
|
|
""s3:DeleteObject""
|
|
],
|
|
""Resource"": [
|
|
""arn:aws:s3:::bucket-name"",
|
|
""arn:aws:s3:::bucket-name/*""
|
|
]
|
|
}
|
|
]
|
|
}
|
|
";
|
|
|
|
public string Key => "s3-iamconfig";
|
|
|
|
public string DisplayName => Strings.S3IAM.DisplayName;
|
|
|
|
public string Description => Strings.S3IAM.Description;
|
|
|
|
|
|
public IList<ICommandLineArgument> SupportedCommands => new List<ICommandLineArgument>([
|
|
new CommandLineArgument(KEY_OPERATION, CommandLineArgument.ArgumentType.Enumeration, Strings.S3IAM.OperationShort, Strings.S3IAM.OperationLong, null, Enum.GetNames(typeof(Operation))),
|
|
new CommandLineArgument(KEY_USERNAME, CommandLineArgument.ArgumentType.String, Strings.S3IAM.UsernameShort, Strings.S3IAM.UsernameLong),
|
|
new CommandLineArgument(KEY_PASSWORD, CommandLineArgument.ArgumentType.String, Strings.S3IAM.PasswordShort, Strings.S3IAM.PasswordLong)
|
|
]);
|
|
|
|
public IDictionary<string, string> Execute(IDictionary<string, string> options)
|
|
{
|
|
options.TryGetValue(KEY_OPERATION, out var operationstring);
|
|
options.TryGetValue(KEY_USERNAME, out var username);
|
|
options.TryGetValue(KEY_PASSWORD, out var password);
|
|
options.TryGetValue(KEY_PATH, out var path);
|
|
|
|
ValidateArgument(operationstring, KEY_OPERATION);
|
|
|
|
if (!Enum.TryParse<Operation>(operationstring, true, out var operation))
|
|
throw new ArgumentException(string.Format("Unable to parse {0} as an operation", operationstring));
|
|
|
|
switch (operation)
|
|
{
|
|
case Operation.GetPolicyDoc:
|
|
ValidateArgument(path, KEY_PATH);
|
|
return GetPolicyDoc(path!);
|
|
|
|
case Operation.CreateIAMUser:
|
|
ValidateArgument(username, KEY_USERNAME);
|
|
ValidateArgument(password, KEY_PASSWORD);
|
|
ValidateArgument(path, KEY_PATH);
|
|
return CreateUnprivilegedUser(username!, password!, path!);
|
|
|
|
default:
|
|
ValidateArgument(username, KEY_USERNAME);
|
|
ValidateArgument(password, KEY_PASSWORD);
|
|
return CanCreateUser(username!, password!);
|
|
}
|
|
}
|
|
|
|
public IDictionary<string, IDictionary<string, string?>> GetLookups()
|
|
=> new Dictionary<string, IDictionary<string, string?>>();
|
|
|
|
private static void ValidateArgument(string? arg, string type)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(arg))
|
|
throw new ArgumentNullException(type);
|
|
}
|
|
|
|
private static IDictionary<string, string> GetPolicyDoc(string path)
|
|
=> new Dictionary<string, string>
|
|
{
|
|
["doc"] = GeneratePolicyDoc(path)
|
|
};
|
|
|
|
private static string GeneratePolicyDoc(string path)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(path))
|
|
throw new ArgumentNullException(nameof(path));
|
|
|
|
path = path.Trim().Trim('/').Trim();
|
|
|
|
if (string.IsNullOrWhiteSpace(path))
|
|
throw new ArgumentException("Invalid value for path");
|
|
|
|
var bucketname = path.Split('/').First();
|
|
|
|
return POLICY_DOCUMENT_TEMPLATE.Replace("bucket-name", bucketname).Trim();
|
|
}
|
|
|
|
|
|
private static bool DetermineIfCreateUserIsAllowed(User user, AmazonIdentityManagementServiceClient cl)
|
|
{
|
|
var simulatePrincipalPolicy = new SimulatePrincipalPolicyRequest
|
|
{
|
|
PolicySourceArn = user.Arn,
|
|
ActionNames = new[] { "iam:CreateUser" }.ToList()
|
|
};
|
|
|
|
return cl.SimulatePrincipalPolicyAsync(simulatePrincipalPolicy)
|
|
.GetAwaiter().GetResult()
|
|
.EvaluationResults.First()
|
|
.EvalDecision == PolicyEvaluationDecisionType.Allowed;
|
|
}
|
|
|
|
private static IDictionary<string, string> GetCreateUserDict(User user, AmazonIdentityManagementServiceClient cl)
|
|
{
|
|
var resultDict = new Dictionary<string, string>
|
|
{
|
|
["isroot"] = false.ToString(),
|
|
["arn"] = user.Arn,
|
|
["id"] = user.UserId,
|
|
["name"] = user.UserName,
|
|
};
|
|
|
|
try
|
|
{
|
|
resultDict["isroot"] = DetermineIfCreateUserIsAllowed(user, cl).ToString();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new Dictionary<string, string>
|
|
{
|
|
// Can be removed if the UI code is updated to check for the "ex" key first
|
|
["isroot"] = false.ToString(),
|
|
["ex"] = ex.ToString(),
|
|
["error"] = $"Exception occurred while testing if CreateUser is allowed for user {user.UserId} : {ex.Message}"
|
|
};
|
|
}
|
|
|
|
return resultDict;
|
|
}
|
|
|
|
private static IDictionary<string, string> CanCreateUser(string awsid, string awskey)
|
|
{
|
|
var cl = new AmazonIdentityManagementServiceClient(awsid, awskey);
|
|
User user;
|
|
try
|
|
{
|
|
user = cl.GetUserAsync().Await().User;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new Dictionary<string, string>
|
|
{
|
|
["isroot"] = false.ToString(),
|
|
["ex"] = ex.ToString(),
|
|
["error"] = $"Exception occurred while retrieving user {awsid} : {ex.Message}"
|
|
};
|
|
}
|
|
|
|
return GetCreateUserDict(user, cl);
|
|
}
|
|
|
|
private static IDictionary<string, string> CreateUnprivilegedUser(string awsid, string awskey, string path)
|
|
{
|
|
var now = Utility.Utility.SerializeDateTime(DateTime.Now);
|
|
var username = string.Format("duplicati-autocreated-backup-user-{0}", now);
|
|
var policyname = string.Format("duplicati-autocreated-policy-{0}", now);
|
|
var policydoc = GeneratePolicyDoc(path);
|
|
|
|
var cl = new AmazonIdentityManagementServiceClient(awsid, awskey);
|
|
var user = cl.CreateUserAsync(new CreateUserRequest(username)).GetAwaiter().GetResult().User;
|
|
cl.PutUserPolicyAsync(new PutUserPolicyRequest(
|
|
user.UserName,
|
|
policyname,
|
|
policydoc
|
|
)).GetAwaiter().GetResult();
|
|
var key = cl.CreateAccessKeyAsync(new CreateAccessKeyRequest { UserName = user.UserName }).GetAwaiter().GetResult().AccessKey;
|
|
|
|
return new Dictionary<string, string>
|
|
{
|
|
["accessid"] = key.AccessKeyId,
|
|
["secretkey"] = key.SecretAccessKey,
|
|
["username"] = key.UserName
|
|
};
|
|
}
|
|
}
|
|
}
|