Skip to content

Commit

Permalink
Merge pull request #190 from nblumhardt/apikey-connect-as
Browse files Browse the repository at this point in the history
Connect as a named user when creating the first API key
  • Loading branch information
nblumhardt authored Apr 29, 2021
2 parents 4dfba88 + 8d3763f commit 8c77d21
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 8 deletions.
88 changes: 83 additions & 5 deletions src/SeqCli/Cli/Commands/ApiKey/CreateCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,17 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Seq.Api;
using Seq.Api.Model.Inputs;
using Seq.Api.Model.LogEvents;
using Seq.Api.Model.Security;
using Seq.Api.Model.Shared;
using SeqCli.Cli.Features;
using SeqCli.Config;
using SeqCli.Connection;
using SeqCli.Levels;
using SeqCli.Util;
using Serilog;

namespace SeqCli.Cli.Commands.ApiKey
{
Expand All @@ -36,8 +39,9 @@ class CreateCommand : Command
readonly PropertiesFeature _properties;
readonly OutputFormatFeature _output;

string _title, _token, _filter, _level;
bool _useServerTimestamps;
string _title, _token, _filter, _level, _connectUsername, _connectPassword;
string[] _permissions;
bool _useServerTimestamps, _connectPasswordStdin;

public CreateCommand(SeqConnectionFactory connectionFactory, SeqCliConfig config)
{
Expand Down Expand Up @@ -70,15 +74,36 @@ public CreateCommand(SeqConnectionFactory connectionFactory, SeqCliConfig config
"Discard client-supplied timestamps and use server clock values",
_ => _useServerTimestamps = true);

Options.Add(
"permissions=",
"The permissions to delegate to the API key; the default is `Ingest`",
v => _permissions = ArgumentString.NormalizeList(v));

Options.Add(
"connect-username=",
"A username to connect with, useful primarily when setting up the first API key",
v => _connectUsername = ArgumentString.Normalize(v));

Options.Add(
"connect-password=",
"When `connect-username` is specified, a corresponding password",
v => _connectPassword = ArgumentString.Normalize(v));

Options.Add(
"connect-password-stdin",
"When `connect-username` is specified, read the corresponding password from `STDIN`",
_ => _connectPasswordStdin = true);

_connection = Enable<ConnectionFeature>();
_output = Enable(new OutputFormatFeature(config.Output));
}

protected override async Task<int> Run()
{
var connection = _connectionFactory.Connect(_connection);

// Default will apply the ingest permission
var connection = await TryConnectAsync();
if (connection == null)
return 1;

var apiKey = await connection.ApiKeys.TemplateAsync();

apiKey.Title = _title;
Expand All @@ -104,6 +129,25 @@ protected override async Task<int> Run()
apiKey.InputSettings.MinimumLevel = Enum.Parse<LogEventLevel>(LevelMapping.ToFullLevelName(_level));
}

apiKey.AssignedPermissions.Clear();
if (_permissions != null)
{
foreach (var permission in _permissions)
{
if (!Enum.TryParse<Permission>(permission, out var p))
{
Log.Error("Unrecognized permission {Permission}", p);
return 1;
}

apiKey.AssignedPermissions.Add(p);
}
}
else
{
apiKey.AssignedPermissions.Add(Permission.Ingest);
}

apiKey = await connection.ApiKeys.AddAsync(apiKey);

if (_token == null && !_output.Json)
Expand All @@ -117,5 +161,39 @@ protected override async Task<int> Run()

return 0;
}

async Task<SeqConnection> TryConnectAsync()
{
SeqConnection connection;
if (_connectUsername != null)
{
if (_connection.IsApiKeySpecified)
{
Log.Error("The `connect-username` and `apikey` options are mutually exclusive");
return null;
}

if (_connectPasswordStdin)
{
if (_connectPassword != null)
{
Log.Error("The `connect-password` and `connect-password-stdin` options are mutually exclusive");
return null;
}

_connectPassword = await Console.In.ReadLineAsync();
}

var (url, _) = _connectionFactory.GetConnectionDetails(_connection);
connection = new SeqConnection(url);
await connection.Users.LoginAsync(_connectUsername, _connectPassword ?? "");
}
else
{
connection = _connectionFactory.Connect(_connection);
}

return connection;
}
}
}
9 changes: 7 additions & 2 deletions src/SeqCli/Cli/Commands/User/CreateCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class CreateCommand : Command
readonly OutputFormatFeature _output;

string _username, _displayName, _roleTitle, _filter, _emailAddress, _password;
bool _passwordStdin;
bool _passwordStdin, _noPasswordChange;

public CreateCommand(SeqConnectionFactory connectionFactory, SeqCliConfig config)
{
Expand Down Expand Up @@ -75,6 +75,11 @@ public CreateCommand(SeqConnectionFactory connectionFactory, SeqCliConfig config
"Read the initial password for the user from `STDIN`, if username/password authentication is in use",
_ => _passwordStdin = true);

Options.Add(
"no-password-change",
"Don't force the user to change their password at next login",
_ => _noPasswordChange = true);

_connection = Enable<ConnectionFeature>();
_output = Enable(new OutputFormatFeature(config.Output));
}
Expand Down Expand Up @@ -110,7 +115,7 @@ protected override async Task<int> Run()
if (_password != null)
{
user.NewPassword = _password;
user.MustChangePassword = true;
user.MustChangePassword = !_noPasswordChange;
}

if (_roleTitle == null)
Expand Down
11 changes: 11 additions & 0 deletions src/SeqCli/Util/ArgumentString.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.

using System.Linq;

namespace SeqCli.Util
{
static class ArgumentString
Expand All @@ -20,5 +22,14 @@ public static string Normalize(string argument)
{
return string.IsNullOrWhiteSpace(argument) ? null : argument.Trim();
}

public static string[] NormalizeList(string argument)
{
return (argument ?? "")
.Split(',')
.Select(Normalize)
.Where(s => s != null)
.ToArray();
}
}
}
33 changes: 33 additions & 0 deletions test/SeqCli.EndToEnd/ApiKey/ApiKeyDelegatePermissionsTestCase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System.Threading.Tasks;
using Seq.Api;
using SeqCli.EndToEnd.Support;
using Serilog;
using Xunit;

namespace SeqCli.EndToEnd.ApiKey
{
[CliTestCase(Multiuser = true)]
public class ApiKeyDelegatePermissionsTestCase : ICliTestCase
{
public Task ExecuteAsync(SeqConnection connection, ILogger logger, CliCommandRunner runner)
{
var exit = runner.Exec(
"user create",
"-n carol -r \"Administrator\" -p test@1234 --no-password-change");
Assert.Equal(0, exit);

exit = runner.Exec(
"apikey create",
"-t Setup --permissions=Setup,Write --connect-username=carol --connect-password=\"test@1234\"");
Assert.Equal(0, exit);

exit = runner.Exec("apikey list", "-t Setup --json --no-color");
Assert.Equal(0, exit);

var output = runner.LastRunProcess.Output;
Assert.Contains("\"AssignedPermissions\": [\"Setup\", \"Write\"]", output);

return Task.CompletedTask;
}
}
}
3 changes: 2 additions & 1 deletion test/SeqCli.EndToEnd/Support/TestDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ public async Task<int> Run()

foreach (var testCaseFactory in _cases.OrderBy(c => Guid.NewGuid()))
{
if (testCaseFactory.Metadata.TryGetValue("Multiuser", out var multiuser) && true.Equals(multiuser) && !_configuration.IsMultiuser)
var isMultiuser = testCaseFactory.Metadata.TryGetValue("Multiuser", out var multiuser) && true.Equals(multiuser);
if (isMultiuser != _configuration.IsMultiuser)
continue;

count++;
Expand Down

0 comments on commit 8c77d21

Please sign in to comment.