diff --git a/common/errors.go b/common/errors.go index 204560e..810e00a 100644 --- a/common/errors.go +++ b/common/errors.go @@ -2,6 +2,7 @@ package common import ( "fmt" + "strings" ) type ParserError interface {} @@ -33,3 +34,10 @@ func (e LineNotFoundError) Error() string { return "Line not found" } +type ValueNotInEnumError struct { + availableValues []string +} +func (e ValueNotInEnumError) Error() string { + return fmt.Sprint("'%s' is not valid. Select one from: %v", strings.Join(e.availableValues, ",")) +} + diff --git a/common/extra-values.go b/common/extra-values.go new file mode 100644 index 0000000..07a3b81 --- /dev/null +++ b/common/extra-values.go @@ -0,0 +1,83 @@ +package common + +import ( + "os" + "strings" +) + +type passwdInfo struct { + Name string + UID string + GID string + HomePath string +} + +var _cachedPasswdInfo []passwdInfo + +func fetchPasswdInfo() ([]passwdInfo, error) { + if len(_cachedPasswdInfo) > 0 { + return _cachedPasswdInfo, nil + } + + readBytes, err := os.ReadFile("/etc/passwd") + + if err != nil { + return []passwdInfo{}, err + } + + lines := strings.Split(string(readBytes), "\n") + infos := make([]passwdInfo, 0) + + for _, line := range lines { + splitted := strings.Split(line, ":") + + if len(splitted) < 6 { + continue + } + + info := passwdInfo{ + Name: splitted[0], + UID: splitted[2], + GID: splitted[3], + HomePath: splitted[5], + } + + infos = append(infos, info) + } + + _cachedPasswdInfo = infos + + return infos, nil +} + + +// UserValue returns a Value that fetches user names from /etc/passwd +// if `separatorForMultiple` is not empty, it will return an ArrayValue +func UserValue(separatorForMultiple string) Value { + return CustomValue{ + FetchValue: func() Value { + infos, err := fetchPasswdInfo() + + if err != nil { + return StringValue{} + } + + enumValues := EnumValue{ + Values: Map(infos, func(info passwdInfo) string { + return info.Name + }), + } + + if separatorForMultiple == "" { + return enumValues + } else { + return ArrayValue{ + AllowDuplicates: false, + SubValue: enumValues, + Separator: separatorForMultiple, + } + } + }, + } +} + diff --git a/common/parser.go b/common/parser.go index c26d239..e3287c2 100644 --- a/common/parser.go +++ b/common/parser.go @@ -111,9 +111,9 @@ func (p *SimpleConfigParser) GetOption(option string) (SimpleConfigLine, error) } } -func (p *SimpleConfigParser) ParseFromFile(content string) []error { +func (p *SimpleConfigParser) ParseFromFile(content string) []ParserError { lines := strings.Split(content, "\n") - errors := make([]error, 0) + errors := make([]ParserError, 0) for index, line := range lines { if p.Options.IgnorePattern.MatchString(line) { diff --git a/common/utils.go b/common/utils.go index 69bef44..9a179cd 100644 --- a/common/utils.go +++ b/common/utils.go @@ -18,3 +18,14 @@ func GetLine(path string, line int) (string, error) { return lines[line], nil } + +func Map[T any, O any](values []T, f func(T) O) []O { + result := make([]O, len(values)) + + for index, value := range values { + result[index] = f(value) + } + + return result +} + diff --git a/handlers/openssh/diagnostics.go b/handlers/openssh/diagnostics.go index d483f97..b232a68 100644 --- a/handlers/openssh/diagnostics.go +++ b/handlers/openssh/diagnostics.go @@ -19,7 +19,7 @@ func ClearDiagnostics(context *glsp.Context, uri protocol.DocumentUri) { ) } -func SendDiagnosticsFromParserErrors(context *glsp.Context, uri protocol.DocumentUri, parserErrors []error) { +func SendDiagnosticsFromParserErrors(context *glsp.Context, uri protocol.DocumentUri, parserErrors []common.ParserError) { diagnosticErrors := make([]protocol.Diagnostic, 0) for _, parserError := range parserErrors { diff --git a/handlers/openssh/documentation.go b/handlers/openssh/documentation.go index a1406e4..a430d1f 100644 --- a/handlers/openssh/documentation.go +++ b/handlers/openssh/documentation.go @@ -52,15 +52,7 @@ See PATTERNS in ssh_config(5) for more information on patterns. This keyword may "AllowUsers": common.NewOption( `This keyword can be followed by a list of user name patterns, separated by spaces. If specified, login is allowed only for user names that match one of the patterns. Only user names are valid; a numerical user ID is not recognized. By default, login is allowed for all users. If the pattern takes the form USER@HOST then USER and HOST are separately checked, restricting logins to particular users from particular hosts. HOST criteria may additionally contain addresses to match in CIDR address/masklen format. The allow/deny users directives are processed in the following order: DenyUsers, AllowUsers. See PATTERNS in ssh_config(5) for more information on patterns. This keyword may appear multiple times in sshd_config with each instance appending to the list.`, - common.CustomValue{ - FetchValue: func() common.Value { - return common.ArrayValue{ - AllowDuplicates: false, - SubValue: common.StringValue{}, - Separator: " ", - } - }, - }, + common.UserValue(" "), ), "AuthenticationMethods": common.NewOption( `Specifies the authentication methods that must be successfully completed for a user to be granted access. This option must be followed by one or more lists of comma-separated authentication method names, or by the single string any to indicate the default behaviour of accepting any single authentication method. If the default is overridden, then successful authentication requires completion of every method in at least one of these lists. @@ -107,10 +99,23 @@ See PATTERNS in ssh_config(5) for more information on patterns. This keyword may common.StringValue{}, ), - // "AuthorizedKeysCommandUser": `Specifies the user under whose account the AuthorizedKeysCommand is run. It is recommended to use a dedicated user that has no other role on the host than running authorized keys commands. If AuthorizedKeysCommand is specified but AuthorizedKeysCommandUser is not, then sshd(8) will refuse to start.`, - // "AuthorizedKeysFile": `Specifies the file that contains the public keys used for user authentication. The format is described in the AUTHORIZED_KEYS FILE FORMAT section of sshd(8). Arguments to AuthorizedKeysFile accept the tokens described in the “TOKENS” section. After expansion, AuthorizedKeysFile is taken to be an absolute path or one relative to the user's home directory. Multiple files may be listed, separated by whitespace. Alternately this option may be set to none to skip checking for user keys in files. The default is ".ssh/authorized_keys .ssh/authorized_keys2".`, - // "AuthorizedPrincipalsCommand": `Specifies a program to be used to generate the list of allowed certificate principals as per AuthorizedPrincipalsFile. The program must be owned by root, not writable by group or others and specified by an absolute path. Arguments to AuthorizedPrincipalsCommand accept the tokens described in the “TOKENS” section. If no arguments are specified then the username of the target user is used. - // The program should produce on standard output zero or more lines of AuthorizedPrincipalsFile output. If either AuthorizedPrincipalsCommand or AuthorizedPrincipalsFile is specified, then certificates offered by the client for authentication must contain a principal that is listed. By default, no AuthorizedPrincipalsCommand is run.`, + "AuthorizedKeysCommandUser": common.NewOption( + `Specifies the user under whose account the AuthorizedKeysCommand is run. It is recommended to use a dedicated user that has no other role on the host than running authorized keys commands. If AuthorizedKeysCommand is specified but AuthorizedKeysCommandUser is not, then sshd(8) will refuse to start.`, + common.UserValue(""), + ), + "AuthorizedKeysFile": common.NewOption( + `Specifies the file that contains the public keys used for user authentication. The format is described in the AUTHORIZED_KEYS FILE FORMAT section of sshd(8). Arguments to AuthorizedKeysFile accept the tokens described in the “TOKENS” section. After expansion, AuthorizedKeysFile is taken to be an absolute path or one relative to the user's home directory. Multiple files may be listed, separated by whitespace. Alternately this option may be set to none to skip checking for user keys in files. The default is ".ssh/authorized_keys .ssh/authorized_keys2".`, + common.ArrayValue{ + SubValue: common.StringValue{}, + Separator: " ", + AllowDuplicates: false, + }, + ), + "AuthorizedPrincipalsCommand": common.NewOption( + `Specifies a program to be used to generate the list of allowed certificate principals as per AuthorizedPrincipalsFile. The program must be owned by root, not writable by group or others and specified by an absolute path. Arguments to AuthorizedPrincipalsCommand accept the tokens described in the “TOKENS” section. If no arguments are specified then the username of the target user is used. + The program should produce on standard output zero or more lines of AuthorizedPrincipalsFile output. If either AuthorizedPrincipalsCommand or AuthorizedPrincipalsFile is specified, then certificates offered by the client for authentication must contain a principal that is listed. By default, no AuthorizedPrincipalsCommand is run.`, + common.StringValue{}, +), // "AuthorizedPrincipalsCommandUser": `Specifies the user under whose account the AuthorizedPrincipalsCommand is run. It is recommended to use a dedicated user that has no other role on the host than running authorized principals commands. If AuthorizedPrincipalsCommand is specified but AuthorizedPrincipalsCommandUser is not, then sshd(8) will refuse to start.`, // "AuthorizedPrincipalsFile": `Specifies a file that lists principal names that are accepted for certificate authentication. When using certificates signed by a key listed in TrustedUserCAKeys, this file lists names, one of which must appear in the certificate for it to be accepted for authentication. Names are listed one per line preceded by key options (as described in “AUTHORIZED_KEYS FILE FORMAT” in sshd(8)). Empty lines and comments starting with ‘#’ are ignored. // Arguments to AuthorizedPrincipalsFile accept the tokens described in the “TOKENS” section. After expansion, AuthorizedPrincipalsFile is taken to be an absolute path or one relative to the user's home directory. The default is none, i.e. not to use a principals file – in this case, the username of the user must appear in a certificate's principals list for it to be accepted. diff --git a/handlers/openssh/text-document-completion.go b/handlers/openssh/text-document-completion.go index 82313b2..44f6d9e 100644 --- a/handlers/openssh/text-document-completion.go +++ b/handlers/openssh/text-document-completion.go @@ -49,15 +49,13 @@ func getRootCompletions() []protocol.CompletionItem { return completions } -func getOptionCompletions(optionName string) []protocol.CompletionItem { - option := Options[optionName] - - switch option.Value.(type) { +func getCompletionsFromValue(value common.Value) []protocol.CompletionItem { + switch value.(type) { case common.EnumValue: - enumOption := option.Value.(common.EnumValue) - completions := make([]protocol.CompletionItem, len(option.Value.(common.EnumValue).Values)) + enumValue := value.(common.EnumValue) + completions := make([]protocol.CompletionItem, len(value.(common.EnumValue).Values)) - for index, value := range enumOption.Values { + for index, value := range enumValue.Values { textFormat := protocol.InsertTextFormatPlainText completions[index] = protocol.CompletionItem{ @@ -66,9 +64,36 @@ func getOptionCompletions(optionName string) []protocol.CompletionItem { } } + return completions + case common.CustomValue: + customValue := value.(common.CustomValue) + val := customValue.FetchValue() + + return getCompletionsFromValue(val) + case common.ArrayValue: + arrayValue := value.(common.ArrayValue) + + return getCompletionsFromValue(arrayValue.SubValue) + case common.OrValue: + orValue := value.(common.OrValue) + + completions := make([]protocol.CompletionItem, 0) + + for _, subValue := range orValue.Values { + completions = append(completions, getCompletionsFromValue(subValue)...) + } + return completions } return []protocol.CompletionItem{} } +func getOptionCompletions(optionName string) []protocol.CompletionItem { + option := Options[optionName] + + completions := getCompletionsFromValue(option.Value) + + return completions +} +