Skip to content

Commit d1dc15b

Browse files
authored
Add auth static provider (user:password) (#42)
* Add static authorization * Add automatic user creation in tests * Update Ydb.Proto dependency, * Add more testcases, add throw of InvalidCredentialsException * Add more verbosity in tests
1 parent 8a6d072 commit d1dc15b

File tree

14 files changed

+474
-17
lines changed

14 files changed

+474
-17
lines changed

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,4 @@ jobs:
6767
- name: Integration test
6868
run: |
6969
cd src
70-
dotnet test --filter "Category=Integration" -f ${{ matrix.dotnet-target-framework }}
70+
dotnet test --filter "Category=Integration" -f ${{ matrix.dotnet-target-framework }} -l "console;verbosity=normal"
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace Ydb.Sdk.Auth;
2+
3+
public interface IUseDriverConfig
4+
{
5+
public Task ProvideConfig(DriverConfig driverConfig);
6+
}
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
using System.IdentityModel.Tokens.Jwt;
2+
using Microsoft.Extensions.Logging;
3+
using Microsoft.Extensions.Logging.Abstractions;
4+
using Ydb.Sdk.Services.Auth;
5+
6+
namespace Ydb.Sdk.Auth;
7+
8+
public class StaticCredentialsProvider : ICredentialsProvider, IUseDriverConfig
9+
{
10+
private readonly ILogger _logger;
11+
12+
private readonly string _user;
13+
private readonly string? _password;
14+
15+
private Driver? _driver;
16+
17+
public int MaxRetries = 5;
18+
19+
private readonly object _lock = new();
20+
21+
private volatile TokenData? _token;
22+
private volatile Task? _refreshTask;
23+
24+
public float RefreshRatio = .1f;
25+
26+
/// <summary>
27+
///
28+
/// </summary>
29+
/// <param name="user">User of the database</param>
30+
/// <param name="password">Password of the user. If user has no password use null </param>
31+
/// <param name="loggerFactory"></param>
32+
public StaticCredentialsProvider(string user, string? password, ILoggerFactory? loggerFactory = null)
33+
{
34+
_user = user;
35+
_password = password;
36+
loggerFactory ??= NullLoggerFactory.Instance;
37+
_logger = loggerFactory.CreateLogger<StaticCredentialsProvider>();
38+
}
39+
40+
private async Task Initialize()
41+
{
42+
_token = await ReceiveToken();
43+
}
44+
45+
public string GetAuthInfo()
46+
{
47+
var token = _token;
48+
49+
if (token is null)
50+
{
51+
lock (_lock)
52+
{
53+
if (_token is not null) return _token.Token;
54+
_logger.LogWarning(
55+
"Blocking for initial token acquirement, please use explicit Initialize async method.");
56+
57+
Initialize().Wait();
58+
59+
return _token!.Token;
60+
}
61+
}
62+
63+
if (token.IsExpired())
64+
{
65+
lock (_lock)
66+
{
67+
if (!_token!.IsExpired()) return _token.Token;
68+
_logger.LogWarning("Blocking on expired token.");
69+
70+
_token = ReceiveToken().Result;
71+
72+
return _token.Token;
73+
}
74+
}
75+
76+
if (!token.IsRefreshNeeded() || _refreshTask is not null) return _token!.Token;
77+
lock (_lock)
78+
{
79+
if (!_token!.IsRefreshNeeded() || _refreshTask is not null) return _token!.Token;
80+
_logger.LogInformation("Refreshing token.");
81+
82+
_refreshTask = Task.Run(RefreshToken);
83+
}
84+
85+
return _token!.Token;
86+
}
87+
88+
private async Task RefreshToken()
89+
{
90+
var token = await ReceiveToken();
91+
92+
lock (_lock)
93+
{
94+
_token = token;
95+
_refreshTask = null;
96+
}
97+
}
98+
99+
private async Task<TokenData> ReceiveToken()
100+
{
101+
var retryAttempt = 0;
102+
while (true)
103+
{
104+
try
105+
{
106+
_logger.LogTrace($"Attempting to receive token, attempt: {retryAttempt}");
107+
108+
var token = await FetchToken();
109+
110+
_logger.LogInformation($"Received token, expires at: {token.ExpiresAt}");
111+
112+
return token;
113+
}
114+
catch (InvalidCredentialsException e)
115+
{
116+
_logger.LogWarning($"Invalid credentials, {e}");
117+
throw;
118+
}
119+
catch (Exception e)
120+
{
121+
_logger.LogDebug($"Failed to fetch token, {e}");
122+
123+
if (retryAttempt >= MaxRetries)
124+
{
125+
_logger.LogWarning($"Can't fetch token, {e}");
126+
throw;
127+
}
128+
129+
await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
130+
_logger.LogInformation($"Failed to fetch token, attempt {retryAttempt}");
131+
++retryAttempt;
132+
}
133+
}
134+
}
135+
136+
private async Task<TokenData> FetchToken()
137+
{
138+
if (_driver is null)
139+
{
140+
_logger.LogError("Driver in for static auth not provided");
141+
throw new NullReferenceException();
142+
}
143+
144+
var client = new AuthClient(_driver);
145+
var loginResponse = await client.Login(_user, _password);
146+
if (loginResponse.Status.StatusCode == StatusCode.Unauthorized)
147+
{
148+
throw new InvalidCredentialsException(Issue.IssuesToString(loginResponse.Status.Issues));
149+
}
150+
151+
loginResponse.Status.EnsureSuccess();
152+
var token = loginResponse.Result.Token;
153+
var jwt = new JwtSecurityToken(token);
154+
return new TokenData(token, jwt.ValidTo, RefreshRatio);
155+
}
156+
157+
public async Task ProvideConfig(DriverConfig driverConfig)
158+
{
159+
_driver = await Driver.CreateInitialized(
160+
new DriverConfig(
161+
driverConfig.Endpoint,
162+
driverConfig.Database,
163+
new AnonymousProvider(),
164+
driverConfig.DefaultTransportTimeout,
165+
driverConfig.DefaultStreamingTransportTimeout,
166+
driverConfig.CustomServerCertificate));
167+
168+
await Initialize();
169+
}
170+
171+
private class TokenData
172+
{
173+
public TokenData(string token, DateTime expiresAt, float refreshInterval)
174+
{
175+
var now = DateTime.UtcNow;
176+
177+
Token = token;
178+
ExpiresAt = expiresAt;
179+
180+
if (expiresAt <= now)
181+
{
182+
RefreshAt = expiresAt;
183+
}
184+
else
185+
{
186+
RefreshAt = now + (expiresAt - now) * refreshInterval;
187+
188+
if (RefreshAt < now)
189+
{
190+
RefreshAt = expiresAt;
191+
}
192+
}
193+
}
194+
195+
public string Token { get; }
196+
public DateTime ExpiresAt { get; }
197+
198+
private DateTime RefreshAt { get; }
199+
200+
public bool IsExpired()
201+
{
202+
return DateTime.UtcNow >= ExpiresAt;
203+
}
204+
205+
public bool IsRefreshNeeded()
206+
{
207+
return DateTime.UtcNow >= RefreshAt;
208+
}
209+
}
210+
}

src/Ydb.Sdk/src/Driver.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Microsoft.Extensions.Logging.Abstractions;
55
using Ydb.Discovery;
66
using Ydb.Discovery.V1;
7+
using Ydb.Sdk.Auth;
78

89
namespace Ydb.Sdk;
910

@@ -72,6 +73,12 @@ public ValueTask DisposeAsync()
7273

7374
public async Task Initialize()
7475
{
76+
if (_config.Credentials is IUseDriverConfig useDriverConfig)
77+
{
78+
await useDriverConfig.ProvideConfig(_config);
79+
_logger.LogInformation("DriverConfig provided to IUseDriverConfig interface");
80+
}
81+
7582
_logger.LogInformation("Started initial endpoint discovery");
7683

7784
try
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using Ydb.Sdk.Client;
2+
3+
namespace Ydb.Sdk.Services.Auth;
4+
5+
public partial class AuthClient : ClientBase
6+
{
7+
public AuthClient(Driver driver) : base(driver)
8+
{
9+
}
10+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
using Ydb.Auth;
2+
using Ydb.Auth.V1;
3+
using Ydb.Sdk.Client;
4+
5+
namespace Ydb.Sdk.Services.Auth;
6+
7+
public class LoginSettings : OperationRequestSettings
8+
{
9+
}
10+
11+
public class LoginResponse : ResponseWithResultBase<LoginResponse.ResultData>
12+
{
13+
internal LoginResponse(Status status, ResultData? result = null)
14+
: base(status, result)
15+
{
16+
}
17+
18+
public class ResultData
19+
{
20+
public string Token { get; }
21+
22+
internal ResultData(string token)
23+
{
24+
Token = token;
25+
}
26+
27+
28+
internal static ResultData FromProto(LoginResult resultProto)
29+
{
30+
var token = resultProto.Token;
31+
return new ResultData(token);
32+
}
33+
}
34+
}
35+
36+
public partial class AuthClient
37+
{
38+
public async Task<LoginResponse> Login(string user, string? password, LoginSettings? settings = null)
39+
{
40+
settings ??= new LoginSettings();
41+
var request = new LoginRequest
42+
{
43+
OperationParams = MakeOperationParams(settings),
44+
User = user
45+
};
46+
if (password is not null)
47+
{
48+
request.Password = password;
49+
}
50+
51+
try
52+
{
53+
var response = await Driver.UnaryCall(
54+
method: AuthService.LoginMethod,
55+
request: request,
56+
settings: settings
57+
);
58+
59+
var status = UnpackOperation(response.Data.Operation, out LoginResult? resultProto);
60+
61+
LoginResponse.ResultData? result = null;
62+
63+
if (status.IsSuccess && resultProto is not null)
64+
{
65+
result = LoginResponse.ResultData.FromProto(resultProto);
66+
}
67+
68+
return new LoginResponse(status, result);
69+
}
70+
catch (Driver.TransportException e)
71+
{
72+
return new LoginResponse(e.Status);
73+
}
74+
}
75+
}

src/Ydb.Sdk/src/Ydb.Sdk.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@
2626
</PropertyGroup>
2727

2828
<ItemGroup>
29-
<PackageReference Include="Ydb.Protos" Version="1.0.3" />
29+
<PackageReference Include="Ydb.Protos" Version="1.0.4" />
3030
<PackageReference Include="Portable.BouncyCastle" Version="1.9.0" />
31+
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.0.0" />
3132
</ItemGroup>
3233

3334
<ItemGroup Condition="$(TargetFramework.Equals('net6.0'))">

0 commit comments

Comments
 (0)