Files
duplicati/Duplicati/Library/Backend/S3/S3IAM.cs
T

233 lines
8.7 KiB
C#

// Copyright (C) 2015, The Duplicati Team
// http://www.duplicati.com, info@duplicati.com
//
// This library is free software; you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as
// published by the Free Software Foundation; either version 2.1 of the
// License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
using System;
using System.Collections.Generic;
using System.Linq;
using Duplicati.Library.Interface;
using Amazon.IdentityManagement;
using Amazon.IdentityManagement.Model;
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 { get { return "s3-iamconfig"; } }
public string DisplayName { get { return "S3 IAM support module"; } }
public string Description { get { return "Exposes S3 IAM manipulation as a web module"; } }
public IList<ICommandLineArgument> SupportedCommands
{
get
{
return new List<ICommandLineArgument>(new ICommandLineArgument[] {
new CommandLineArgument(KEY_OPERATION, CommandLineArgument.ArgumentType.Enumeration, "The operation to perform", "Selects the operation to perform", null, Enum.GetNames(typeof(Operation))),
new CommandLineArgument(KEY_USERNAME, CommandLineArgument.ArgumentType.String, "The username", "The Amazon Access Key ID"),
new CommandLineArgument(KEY_PASSWORD, CommandLineArgument.ArgumentType.String, "The password", "The Amazon Secret Key"),
});
}
}
public IDictionary<string, string> Execute(IDictionary<string, string> options)
{
options.TryGetValue(KEY_OPERATION, out string operationstring);
options.TryGetValue(KEY_USERNAME, out string username);
options.TryGetValue(KEY_PASSWORD, out string password);
options.TryGetValue(KEY_PATH, out string path);
ValidateArgument(operationstring, KEY_OPERATION);
if (!Enum.TryParse(operationstring, true, out Operation 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);
}
}
private static void ValidateArgument(string arg, string type)
{
if (string.IsNullOrWhiteSpace(arg))
{
throw new ArgumentNullException(type);
}
}
private static IDictionary<string, string> GetPolicyDoc(string path)
{
return 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 Boolean DetermineIfCreateUserIsAllowed(User user, AmazonIdentityManagementServiceClient cl)
{
var simulatePrincipalPolicy = new SimulatePrincipalPolicyRequest
{
PolicySourceArn = user.Arn,
ActionNames = new[] { "iam:CreateUser" }.ToList()
};
return cl.SimulatePrincipalPolicy(simulatePrincipalPolicy).
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.GetUser().User;
}
catch (Exception ex) when (ex is NoSuchEntityException || ex is ServiceFailureException)
{
return new Dictionary<string, string>
{
["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.CreateUser(new CreateUserRequest(username)).User;
cl.PutUserPolicy(new PutUserPolicyRequest(
user.UserName,
policyname,
policydoc
));
var key = cl.CreateAccessKey(new CreateAccessKeyRequest { UserName = user.UserName }).AccessKey;
return new Dictionary<string, string>
{
["accessid"] = key.AccessKeyId,
["secretkey"] = key.SecretAccessKey,
["username"] = key.UserName
};
}
}
}