2024-03-04 12:21:53 +01:00
// Copyright (C) 2024, The Duplicati Team
2024-02-28 15:45:30 +01:00
// 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.
2016-06-22 20:22:59 +02:00
using System ;
2016-06-22 23:14:18 +02:00
using System.Collections.Generic ;
using System.Linq ;
using Duplicati.Library.Interface ;
using Amazon.IdentityManagement ;
using Amazon.IdentityManagement.Model ;
2016-06-22 20:22:59 +02:00
namespace Duplicati.Library.Backend
{
2016-09-15 11:39:27 +02:00
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
}
2019-01-12 17:08:08 +01:00
public const string POLICY_DOCUMENT_TEMPLATE =
2016-06-22 23:14:18 +02:00
@"
{
""Version"": ""2012-10-17"",
""Statement"": [
{
""Sid"": ""Stmt1390497858034"",
""Effect"": ""Allow"",
""Action"": [
""s3:GetObject"",
""s3:PutObject"",
""s3:ListBucket"",
""s3:DeleteObject""
],
""Resource"": [
2017-09-16 20:29:44 +02:00
""arn:aws:s3:::bucket-name"",
""arn:aws:s3:::bucket-name/*""
2016-06-22 23:14:18 +02:00
]
}
]
}
" ;
2016-09-15 11:39:27 +02:00
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 )
{
2019-01-12 16:10:34 +01:00
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 );
2016-09-15 11:39:27 +02:00
2019-01-12 17:08:08 +01:00
ValidateArgument ( operationstring , KEY_OPERATION );
2016-09-15 11:39:27 +02:00
2019-01-12 16:10:34 +01:00
if (! Enum . TryParse ( operationstring , true , out Operation operation ))
2016-09-15 11:39:27 +02:00
throw new ArgumentException ( string . Format ( "Unable to parse {0} as an operation" , operationstring ));
switch ( operation )
{
case Operation . GetPolicyDoc :
2019-01-12 17:08:08 +01:00
ValidateArgument ( path , KEY_PATH );
2016-09-15 11:39:27 +02:00
return GetPolicyDoc ( path );
case Operation . CreateIAMUser :
2019-01-12 17:08:08 +01:00
ValidateArgument ( username , KEY_USERNAME );
ValidateArgument ( password , KEY_PASSWORD );
ValidateArgument ( path , KEY_PATH );
2016-09-15 11:39:27 +02:00
return CreateUnprivilegedUser ( username , password , path );
default :
2019-01-12 17:08:08 +01:00
ValidateArgument ( username , KEY_USERNAME );
ValidateArgument ( password , KEY_PASSWORD );
2016-09-15 11:39:27 +02:00
return CanCreateUser ( username , password );
}
}
2019-01-12 17:08:08 +01:00
private static void ValidateArgument ( string arg , string type )
{
if ( string . IsNullOrWhiteSpace ( arg ))
{
throw new ArgumentNullException ( type );
}
}
2019-01-12 15:58:57 +01:00
private static IDictionary < string , string > GetPolicyDoc ( string path )
2016-09-15 11:39:27 +02:00
{
2019-01-12 15:58:57 +01:00
return new Dictionary < string , string >
2019-01-06 18:41:57 +01:00
{
["doc"] = GeneratePolicyDoc ( path )
};
2016-09-15 11:39:27 +02:00
}
2019-01-12 15:58:57 +01:00
private static string GeneratePolicyDoc ( string path )
2016-09-15 11:39:27 +02:00
{
if ( string . IsNullOrWhiteSpace ( path ))
2018-03-07 17:53:08 -08:00
throw new ArgumentNullException ( nameof ( path ));
2016-09-15 11:39:27 +02:00
path = path . Trim (). Trim ( '/' ). Trim ();
if ( string . IsNullOrWhiteSpace ( path ))
throw new ArgumentException ( "Invalid value for path" );
2017-09-16 20:29:44 +02:00
var bucketname = path . Split ( '/' ). First ();
return POLICY_DOCUMENT_TEMPLATE . Replace ( "bucket-name" , bucketname ). Trim ();
2016-09-15 11:39:27 +02:00
}
2019-01-12 17:08:08 +01:00
private static Boolean DetermineIfCreateUserIsAllowed ( User user , AmazonIdentityManagementServiceClient cl )
2016-09-15 11:39:27 +02:00
{
2019-01-12 17:08:08 +01:00
var simulatePrincipalPolicy = new SimulatePrincipalPolicyRequest
2016-09-15 11:39:27 +02:00
{
2019-01-12 17:08:08 +01:00
PolicySourceArn = user . Arn ,
ActionNames = new [] { "iam:CreateUser" }. ToList ()
};
2024-03-01 14:30:28 +01:00
return cl . SimulatePrincipalPolicyAsync ( simulatePrincipalPolicy )
. GetAwaiter (). GetResult ()
. EvaluationResults . First ()
. EvalDecision == PolicyEvaluationDecisionType . Allowed ;
2019-01-12 17:08:08 +01:00
}
private static IDictionary < string , string > GetCreateUserDict ( User user , AmazonIdentityManagementServiceClient cl )
{
2019-01-12 15:46:30 +01:00
var resultDict = new Dictionary < string , string >
{
2019-01-16 14:22:24 +01:00
["isroot"] = false . ToString (),
2019-01-12 15:46:30 +01:00
["arn"] = user . Arn ,
["id"] = user . UserId ,
["name"] = user . UserName ,
};
try
{
2019-01-12 17:08:08 +01:00
resultDict [ "isroot" ] = DetermineIfCreateUserIsAllowed ( user , cl ). ToString ();
2019-01-12 15:46:30 +01:00
}
catch ( Exception ex )
{
2019-01-12 17:08:08 +01:00
return new Dictionary < string , string >
{
2019-01-16 14:22:24 +01:00
// Can be removed if the UI code is updated to check for the "ex" key first
["isroot"] = false . ToString (),
2019-01-12 17:08:08 +01:00
["ex"] = ex . ToString (),
["error"] = $"Exception occurred while testing if CreateUser is allowed for user {user.UserId} : {ex.Message}"
};
2019-01-12 15:46:30 +01:00
}
return resultDict ;
2016-09-15 11:39:27 +02:00
}
2019-01-12 17:08:08 +01:00
private static IDictionary < string , string > CanCreateUser ( string awsid , string awskey )
{
var cl = new AmazonIdentityManagementServiceClient ( awsid , awskey );
User user ;
try
{
2024-03-01 14:30:28 +01:00
user = cl . GetUserAsync (). GetAwaiter (). GetResult (). User ;
2019-01-12 17:08:08 +01:00
}
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 );
}
2019-01-12 15:58:57 +01:00
private static IDictionary < string , string > CreateUnprivilegedUser ( string awsid , string awskey , string path )
2016-09-15 11:39:27 +02:00
{
2019-01-11 13:19:36 +01:00
var now = Utility . Utility . SerializeDateTime ( DateTime . Now );
2016-09-15 11:39:27 +02:00
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 );
2024-03-01 14:30:28 +01:00
var user = cl . CreateUserAsync ( new CreateUserRequest ( username )). GetAwaiter (). GetResult (). User ;
cl . PutUserPolicyAsync ( new PutUserPolicyRequest (
2016-09-15 11:39:27 +02:00
user . UserName ,
policyname ,
policydoc
2024-03-01 14:30:28 +01:00
)). GetAwaiter (). GetResult ();
var key = cl . CreateAccessKeyAsync ( new CreateAccessKeyRequest { UserName = user . UserName }). GetAwaiter (). GetResult (). AccessKey ;
2016-09-15 11:39:27 +02:00
2019-01-11 13:19:36 +01:00
return new Dictionary < string , string >
2019-01-06 18:41:57 +01:00
{
["accessid"] = key . AccessKeyId ,
["secretkey"] = key . SecretAccessKey ,
["username"] = key . UserName
};
2016-09-15 11:39:27 +02:00
}
}
2016-06-22 20:22:59 +02:00
}