Skip to content

Commit ec85a83

Browse files
committed
Merge pull request #201 from alanquillin/FixUserImpersonation
Fixed bug with user impersonation and added List Endpoints method
2 parents 67c93f5 + 429f420 commit ec85a83

File tree

10 files changed

+264
-19
lines changed

10 files changed

+264
-19
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
using System.Diagnostics;
2+
using Newtonsoft.Json;
3+
4+
namespace net.openstack.Core.Domain
5+
{
6+
/// <summary>
7+
/// Represents an endpoint for a tenant that is returned outside of the <see cref="ServiceCatalog"/>.
8+
/// </summary>
9+
/// <threadsafety static="true" instance="false"/>
10+
/// <seealso href="http://docs.openstack.org/api/openstack-identity-service/2.0/content/GET_listEndpointsForToken_v2.0_tokens__tokenId__endpoints_Token_Operations.html">List Token Endpoints (OpenStack Identity Service API v2.0 Reference)</seealso>
11+
[JsonObject(MemberSerialization.OptIn)]
12+
[DebuggerDisplay("{Name,nq} ({Type,nq})")]
13+
public class ExtendedEndpoint : Endpoint
14+
{
15+
/// <summary>
16+
/// Gets the id of the endpoint, which may be a vendor-specific id.
17+
/// </summary>
18+
/// <seealso href="http://docs.openstack.org/api/openstack-identity-service/2.0/content/GET_listEndpointsForToken_v2.0_tokens__tokenId__endpoints_Token_Operations.html">List Token Endpoints (OpenStack Identity Service API v2.0 Reference)</seealso>
19+
[JsonProperty("id")]
20+
public string Id { get; private set; }
21+
22+
/// <summary>
23+
/// Gets the display name of the service, which may be a vendor-specific
24+
/// product name.
25+
/// </summary>
26+
/// <seealso href="http://docs.openstack.org/api/openstack-identity-service/2.0/content/GET_listEndpointsForToken_v2.0_tokens__tokenId__endpoints_Token_Operations.html">List Token Endpoints (OpenStack Identity Service API v2.0 Reference)</seealso>
27+
[JsonProperty("name")]
28+
public string Name { get; private set; }
29+
30+
/// <summary>
31+
/// Gets the canonical name of the specification implemented by this service.
32+
/// </summary>
33+
/// <seealso href="http://docs.openstack.org/api/openstack-identity-service/2.0/content/GET_listEndpointsForToken_v2.0_tokens__tokenId__endpoints_Token_Operations.html">List Token Endpoint (OpenStack Identity Service API v2.0 Reference)</seealso>
34+
[JsonProperty("type")]
35+
public string Type { get; private set; }
36+
}
37+
}

src/corelib/Core/Domain/ServiceCatalog.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,31 @@ namespace net.openstack.Core.Domain
1414
[DebuggerDisplay("{Name,nq} ({Type,nq})")]
1515
public class ServiceCatalog
1616
{
17+
/// <summary>
18+
/// Initializes a new instance of the <see cref="ServiceCatalog"/> class.
19+
/// </summary>
20+
/// <remarks>
21+
/// This constructor is used by the JSON deserializer.
22+
/// </remarks>
23+
[JsonConstructor]
24+
protected ServiceCatalog()
25+
{
26+
}
27+
28+
/// <summary>
29+
/// Initializes a new instance of the <see cref="ServiceCatalog"/> class
30+
/// with the specified name, username, and endpoints.
31+
/// </summary>
32+
/// <param name="name">The display name of the service.</param>
33+
/// <param name="type">The canonical name of the service.</param>
34+
/// <param name="endpoints">A collection of <see cref="Endpoint"/> objects describing the service endpoints.</param>
35+
public ServiceCatalog(string name, string type, Endpoint[] endpoints)
36+
{
37+
Name = name;
38+
Type = type;
39+
Endpoints = endpoints;
40+
}
41+
1742
/// <summary>
1843
/// Gets the endpoints for the service.
1944
/// </summary>

src/corelib/Core/Domain/UserAccess.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,31 @@ namespace net.openstack.Core.Domain
1010
[JsonObject(MemberSerialization.OptIn)]
1111
public class UserAccess
1212
{
13+
/// <summary>
14+
/// Initializes a new instance of the <see cref="UserAccess"/> class.
15+
/// </summary>
16+
/// <remarks>
17+
/// This constructor is used by the JSON deserializer.
18+
/// </remarks>
19+
[JsonConstructor]
20+
protected UserAccess()
21+
{
22+
}
23+
24+
/// <summary>
25+
/// Initializes a new instance of the <see cref="UserAccess"/> class
26+
/// with the specified token, user, and service catalog.
27+
/// </summary>
28+
/// <param name="token">The <see cref="IdentityToken "/>.</param>
29+
/// <param name="user">The <see cref="UserDetails"/>.</param>
30+
/// <param name="serviceCatalog">List of <see cref="ServiceCatalog"/>s.</param>
31+
public UserAccess(IdentityToken token, UserDetails user, ServiceCatalog[] serviceCatalog)
32+
{
33+
Token = token;
34+
User = user;
35+
ServiceCatalog = serviceCatalog;
36+
}
37+
1338
/// <summary>
1439
/// Gets the <see cref="IdentityToken"/> which allows providers to make authenticated
1540
/// calls to API methods.

src/corelib/Core/Providers/IIdentityProvider.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,20 @@ public interface IIdentityProvider
268268
/// <seealso href="">Get User Credentials (OpenStack Identity Service API v2.0 Reference)</seealso>
269269
UserCredential GetUserCredential(string userId, string credentialKey, CloudIdentity identity = null);
270270

271+
/// <summary>
272+
/// Lists the endpoints associated to a given authentication token.
273+
/// </summary>
274+
/// <param name="token">The authentication token Id. This is obtained from <see cref="IdentityToken.Id"/></param>
275+
/// <param name="identity">The cloud identity to use for this request. If not specified, the default identity for the current provider instance will be used.</param>
276+
/// <returns>A collection of <see cref="ExtendedEndpoint"/> objects containing endpoint details.</returns>
277+
/// <exception cref="ArgumentNullException">If <paramref name="token"/> is <c>null</c>.</exception>
278+
/// <exception cref="ArgumentException">If <paramref name="token"/> is empty.</exception>
279+
/// <exception cref="NotSupportedException">If the provider does not support the given <paramref name="identity"/> type.</exception>
280+
/// <exception cref="InvalidOperationException">If <paramref name="identity"/> is <c>null</c> and no default identity is available for the provider.</exception>
281+
/// <exception cref="ResponseException">If the authentication request failed or the token does not exist.</exception>
282+
/// <seealso href="http://docs.openstack.org/api/openstack-identity-service/2.0/content/GET_listEndpointsForToken_v2.0_tokens__tokenId__endpoints_Token_Operations.html">List Token Endpoints (OpenStack Identity Service API v2.0 Reference)</seealso>
283+
IEnumerable<ExtendedEndpoint> ListEndpoints(string token, CloudIdentity identity = null);
284+
271285
/// <summary>
272286
/// Gets the default <see cref="CloudIdentity"/> to use for requests from this provider.
273287
/// If no default identity is available, the value is <c>null</c>.

src/corelib/Providers/Rackspace/CloudIdentityProvider.cs

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -681,6 +681,9 @@ public UserAccess GetUserAccess(CloudIdentity identity, bool forceCacheRefresh =
681681
if (identity == null)
682682
identity = DefaultIdentity;
683683

684+
if (identity is RackspaceImpersonationIdentity)
685+
return Impersonate(identity as RackspaceImpersonationIdentity, forceCacheRefresh);
686+
684687
var rackspaceCloudIdentity = identity as RackspaceCloudIdentity;
685688

686689
if (rackspaceCloudIdentity == null)
@@ -718,25 +721,54 @@ public UserAccess GetUserAccess(CloudIdentity identity, bool forceCacheRefresh =
718721
/// <exception cref="NotSupportedException">If the provider does not support the given <paramref name="identity"/> type.</exception>
719722
/// <exception cref="InvalidOperationException">If <paramref name="identity"/> is <c>null</c> and no default identity is available for the provider.</exception>
720723
/// <exception cref="ResponseException">If the authentication request failed.</exception>
721-
public UserAccess Impersonate(RackspaceImpersonationIdentity identity, bool forceCacheRefresh = false)
724+
private UserAccess Impersonate(RackspaceImpersonationIdentity identity, bool forceCacheRefresh = false)
722725
{
723726
if (identity == null)
724727
throw new ArgumentNullException("identity");
725728

726-
var impToken = _userAccessCache.Get(string.Format("imp/{0}/{1}", identity.UserToImpersonate.Domain, identity.UserToImpersonate.Username), () => {
729+
var impToken = _userAccessCache.Get(string.Format("{0}/imp/{1}/{2}", identity.Username, identity.UserToImpersonate.Domain == null ? "none" : identity.UserToImpersonate.Domain.Name, identity.UserToImpersonate.Username), () =>
730+
{
727731
const string urlPath = "/v2.0/RAX-AUTH/impersonation-tokens";
728-
var request = BuildImpersonationRequestJson(urlPath, identity.UserToImpersonate.Username, 600);
729-
var response = ExecuteRESTRequest<UserImpersonationResponse>(identity, new Uri(_urlBase, urlPath), HttpMethod.POST, request);
730-
732+
var request = BuildImpersonationRequestJson(identity.UserToImpersonate.Username, 600);
733+
var parentIdentity = new RackspaceCloudIdentity(identity);
734+
var response = ExecuteRESTRequest<UserImpersonationResponse>(parentIdentity, new Uri(_urlBase, urlPath), HttpMethod.POST, request);
731735
if (response == null || response.Data == null || response.Data.UserAccess == null)
732736
return null;
733737

734-
return response.Data.UserAccess;
738+
IdentityToken impersonationToken = response.Data.UserAccess.Token;
739+
if (impersonationToken == null)
740+
return null;
741+
742+
var userAccess = ValidateToken(impersonationToken.Id, identity: parentIdentity);
743+
if (userAccess == null)
744+
return null;
745+
746+
var endpoints = ListEndpoints(impersonationToken.Id, parentIdentity);
747+
748+
var serviceCatalog = BuildServiceCatalog(endpoints);
749+
750+
return new UserAccess(userAccess.Token, userAccess.User, serviceCatalog);
735751
}, forceCacheRefresh);
736752

737753
return impToken;
738754
}
739755

756+
private static ServiceCatalog[] BuildServiceCatalog(IEnumerable<ExtendedEndpoint> endpoints)
757+
{
758+
var serviceCatalog = new List<ServiceCatalog>();
759+
var services = endpoints.Select(e => Tuple.Create(e.Type, e.Name)).Distinct();
760+
761+
foreach (var service in services)
762+
{
763+
string type = service.Item1;
764+
string name = service.Item2;
765+
IEnumerable<ExtendedEndpoint> serviceEndpoints = endpoints.Where(endpoint => string.Equals(type, endpoint.Type, StringComparison.OrdinalIgnoreCase) && string.Equals(name, endpoint.Name, StringComparison.OrdinalIgnoreCase));
766+
serviceCatalog.Add(new ServiceCatalog(name, type, serviceEndpoints.ToArray()));
767+
}
768+
769+
return serviceCatalog.ToArray();
770+
}
771+
740772
/// <inheritdoc/>
741773
public UserAccess ValidateToken(string token, string tenantId = null, CloudIdentity identity = null)
742774
{
@@ -759,14 +791,30 @@ public UserAccess ValidateToken(string token, string tenantId = null, CloudIdent
759791
return response.Data.UserAccess;
760792
}
761793

762-
private JObject BuildImpersonationRequestJson(string path, string userName, int expirationInSeconds)
794+
/// <inheritdoc/>
795+
public IEnumerable<ExtendedEndpoint> ListEndpoints(string token, CloudIdentity identity = null)
796+
{
797+
if (token == null)
798+
throw new ArgumentNullException("token");
799+
if (string.IsNullOrEmpty(token))
800+
throw new ArgumentException("token cannot be empty");
801+
802+
var response = ExecuteRESTRequest<ListEndpointsResponse>(identity, new Uri(_urlBase, string.Format("/v2.0/tokens/{0}/endpoints", token)), HttpMethod.GET);
803+
804+
805+
if (response == null || response.Data == null)
806+
return null;
807+
808+
return response.Data.Endpoints;
809+
}
810+
811+
private JObject BuildImpersonationRequestJson(string userName, int expirationInSeconds)
763812
{
764813
var request = new JObject();
765814
var impInfo = new JObject();
766815
var user = new JObject { { "username", userName }, { "expire-in-seconds", expirationInSeconds } };
767816
impInfo.Add("user", user);
768-
var parts = path.Split('/');
769-
request.Add(string.Format("{0}:impersonation", parts[1]), impInfo);
817+
request.Add("RAX-AUTH:impersonation", impInfo);
770818

771819
return request;
772820
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using Newtonsoft.Json;
2+
using net.openstack.Core.Domain;
3+
4+
namespace net.openstack.Providers.Rackspace.Objects.Response
5+
{
6+
/// <summary>
7+
/// This models the JSON response used for the List Endpoints request.
8+
/// </summary>
9+
/// <seealso href="hhttp://docs.openstack.org/api/openstack-identity-service/2.0/content/GET_listEndpointsForToken_v2.0_tokens__tokenId__endpoints_Token_Operations.html">List Token Endpoints (OpenStack Identity Service API v2.0 Reference)</seealso>
10+
/// <threadsafety static="true" instance="false"/>
11+
[JsonObject(MemberSerialization.OptIn)]
12+
internal class ListEndpointsResponse
13+
{
14+
/// <summary>
15+
/// Gets additional information about the endpoints.
16+
/// </summary>
17+
/// <seealso cref="UserAccess"/>
18+
[JsonProperty("endpoints")]
19+
public ExtendedEndpoint[] Endpoints { get; private set; }
20+
}
21+
}

src/corelib/Providers/Rackspace/Objects/Response/UserImpersonationResponse.cs

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,41 @@ namespace net.openstack.Providers.Rackspace.Objects.Response
33
using net.openstack.Core.Domain;
44
using Newtonsoft.Json;
55

6+
/// <summary>
7+
/// This models the JSON response used for the Impersonate User request.
8+
/// </summary>
9+
/// <remarks>
10+
/// The Impersonate User API is a Rackspace-specific extension to the OpenStack
11+
/// Identity Service, and is documented in the Rackspace <strong>Cloud Identity
12+
/// Admin Developer Guide - API v2.0</strong>.
13+
/// </remarks>
14+
/// <threadsafety static="true" instance="false"/>
615
[JsonObject(MemberSerialization.OptIn)]
716
internal class UserImpersonationResponse
817
{
9-
[JsonProperty("userAccess")]
10-
public UserAccess UserAccess { get; private set; }
18+
/// <summary>
19+
/// Gets the details for the response.
20+
/// </summary>
21+
[JsonProperty("access")]
22+
public UserImpersonationData UserAccess { get; private set; }
23+
24+
/// <summary>
25+
/// This models the JSON body containing details for the Impersonate User response.
26+
/// </summary>
27+
/// <threadsafety static="true" instance="false"/>
28+
[JsonObject(MemberSerialization.OptIn)]
29+
internal class UserImpersonationData
30+
{
31+
/// <summary>
32+
/// Gets the <see cref="IdentityToken"/> which allows providers to make
33+
/// impersonated calls to API methods.
34+
/// </summary>
35+
[JsonProperty("token")]
36+
public IdentityToken Token
37+
{
38+
get;
39+
private set;
40+
}
41+
}
1142
}
1243
}

src/corelib/corelib.v3.5.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
<Compile Include="Core\Domain\Converters\SimpleStringJsonConverter`1.cs" />
6666
<Compile Include="Core\Domain\Converters\IPAddressSimpleConverter.cs" />
6767
<Compile Include="Core\Domain\DiskConfiguration.cs" />
68+
<Compile Include="Core\Domain\ExtendedEndpoint.cs" />
6869
<Compile Include="Core\Domain\ImageState.cs" />
6970
<Compile Include="Core\Domain\IPAddressList.cs" />
7071
<Compile Include="Core\Domain\PowerState.cs" />
@@ -157,6 +158,7 @@
157158
<Compile Include="Providers\Rackspace\Objects\Request\NamespaceDoc.cs" />
158159
<Compile Include="Providers\Rackspace\Objects\Request\PasswordCredential.cs" />
159160
<Compile Include="Providers\Rackspace\Objects\Response\BulkDeleteResponse.cs" />
161+
<Compile Include="Providers\Rackspace\Objects\Response\ListEndpointsResponse.cs" />
160162
<Compile Include="Providers\Rackspace\Objects\Response\MetadataItemResponse.cs" />
161163
<Compile Include="Providers\Rackspace\Objects\Response\ListVirtualInterfacesResponse.cs" />
162164
<Compile Include="Providers\Rackspace\Objects\Response\NamespaceDoc.cs" />

src/corelib/corelib.v4.0.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
<Compile Include="Core\Domain\Converters\SimpleStringJsonConverter`1.cs" />
5656
<Compile Include="Core\Domain\Converters\IPAddressSimpleConverter.cs" />
5757
<Compile Include="Core\Domain\DiskConfiguration.cs" />
58+
<Compile Include="Core\Domain\ExtendedEndpoint.cs" />
5859
<Compile Include="Core\Domain\ImageState.cs" />
5960
<Compile Include="Core\Domain\IPAddressList.cs" />
6061
<Compile Include="Core\Domain\PowerState.cs" />
@@ -147,6 +148,7 @@
147148
<Compile Include="Providers\Rackspace\Objects\Request\NamespaceDoc.cs" />
148149
<Compile Include="Providers\Rackspace\Objects\Request\PasswordCredential.cs" />
149150
<Compile Include="Providers\Rackspace\Objects\Response\BulkDeleteResponse.cs" />
151+
<Compile Include="Providers\Rackspace\Objects\Response\ListEndpointsResponse.cs" />
150152
<Compile Include="Providers\Rackspace\Objects\Response\MetadataItemResponse.cs" />
151153
<Compile Include="Providers\Rackspace\Objects\Response\ListVirtualInterfacesResponse.cs" />
152154
<Compile Include="Providers\Rackspace\Objects\Response\NamespaceDoc.cs" />

src/testing/integration/Providers/Rackspace/UserIdentityTests.cs

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using net.openstack.Core.Providers;
1010
using net.openstack.Providers.Rackspace;
1111
using Newtonsoft.Json;
12+
using HttpStatusCode = System.Net.HttpStatusCode;
1213
using Path = System.IO.Path;
1314

1415
/// <summary>
@@ -84,15 +85,54 @@ public void TestValidateToken()
8485
Assert.IsNotNull(userAccess.Token);
8586
Assert.IsNotNull(userAccess.Token.Id);
8687

87-
UserAccess validated = provider.ValidateToken(userAccess.Token.Id);
88-
Assert.IsNotNull(validated);
89-
Assert.IsNotNull(validated.Token);
90-
Assert.AreEqual(userAccess.Token.Id, validated.Token.Id);
88+
try
89+
{
90+
UserAccess validated = provider.ValidateToken(userAccess.Token.Id);
91+
Assert.IsNotNull(validated);
92+
Assert.IsNotNull(validated.Token);
93+
Assert.AreEqual(userAccess.Token.Id, validated.Token.Id);
94+
95+
Assert.IsNotNull(validated.User);
96+
Assert.AreEqual(userAccess.User.Id, validated.User.Id);
97+
Assert.AreEqual(userAccess.User.Name, validated.User.Name);
98+
Assert.AreEqual(userAccess.User.DefaultRegion, validated.User.DefaultRegion);
99+
}
100+
catch (UserNotAuthorizedException ex)
101+
{
102+
if (ex.Response.StatusCode != HttpStatusCode.Forbidden)
103+
throw;
104+
105+
Assert.Inconclusive("The service does not allow this user to access the Validate Token API.");
106+
}
107+
}
91108

92-
Assert.IsNotNull(validated.User);
93-
Assert.AreEqual(userAccess.User.Id, validated.User.Id);
94-
Assert.AreEqual(userAccess.User.Name, validated.User.Name);
95-
Assert.AreEqual(userAccess.User.DefaultRegion, validated.User.DefaultRegion);
109+
/// <summary>
110+
/// This method tests the basic functionality of the <see cref="IIdentityProvider.ListEndpoints"/>
111+
/// method for an authenticated user.
112+
/// </summary>
113+
[TestMethod]
114+
[TestCategory(TestCategories.User)]
115+
[TestCategory(TestCategories.Identity)]
116+
public void TestListEndpoints()
117+
{
118+
IIdentityProvider provider = new CloudIdentityProvider(Bootstrapper.Settings.TestIdentity);
119+
UserAccess userAccess = provider.Authenticate();
120+
Assert.IsNotNull(userAccess);
121+
Assert.IsNotNull(userAccess.Token);
122+
Assert.IsNotNull(userAccess.Token.Id);
123+
124+
try
125+
{
126+
IEnumerable<ExtendedEndpoint> endpoints = provider.ListEndpoints(userAccess.Token.Id);
127+
Console.WriteLine(JsonConvert.SerializeObject(userAccess, Formatting.Indented));
128+
}
129+
catch (UserNotAuthorizedException ex)
130+
{
131+
if (ex.Response.StatusCode != HttpStatusCode.Forbidden)
132+
throw;
133+
134+
Assert.Inconclusive("The service does not allow this user to access the List Endpoints API.");
135+
}
96136
}
97137

98138
[TestMethod]

0 commit comments

Comments
 (0)