mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-25 19:15:34 +00:00
cmd/tailscale/web: add support for QNAP
Signed-off-by: Maisem Ali <maisem@tailscale.com>
This commit is contained in:
parent
8b11937eaf
commit
f944614c5c
57
cmd/tailscale/cli/auth-redirect.html
Normal file
57
cmd/tailscale/cli/auth-redirect.html
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Redirecting...</title>
|
||||||
|
<style>
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
background-color: rgb(249, 247, 246);
|
||||||
|
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||||
|
line-height: 1.5;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
border: 4px rgba(112, 110, 109, 0.5) solid;
|
||||||
|
border-left-color: transparent;
|
||||||
|
border-radius: 9999px;
|
||||||
|
width: 4rem;
|
||||||
|
height: 4rem;
|
||||||
|
-webkit-animation: spin 700ms linear infinite;
|
||||||
|
animation: spin 800ms linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
color: rgb(112, 110, 109);
|
||||||
|
padding-left: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head> <body>
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<div class="label">Redirecting...</div>
|
||||||
|
</body>
|
@ -5,28 +5,29 @@
|
|||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"encoding/xml"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/cgi"
|
"net/http/cgi"
|
||||||
"os"
|
"net/url"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/peterbourgon/ff/v2/ffcli"
|
"github.com/peterbourgon/ff/v2/ffcli"
|
||||||
"go4.org/mem"
|
|
||||||
"tailscale.com/client/tailscale"
|
"tailscale.com/client/tailscale"
|
||||||
"tailscale.com/ipn"
|
"tailscale.com/ipn"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/types/preftype"
|
"tailscale.com/types/preftype"
|
||||||
|
"tailscale.com/util/groupmember"
|
||||||
"tailscale.com/version/distro"
|
"tailscale.com/version/distro"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -36,6 +37,9 @@
|
|||||||
//go:embed web.css
|
//go:embed web.css
|
||||||
var webCSS string
|
var webCSS string
|
||||||
|
|
||||||
|
//go:embed auth-redirect.html
|
||||||
|
var authenticationRedirectHTML string
|
||||||
|
|
||||||
var tmpl *template.Template
|
var tmpl *template.Template
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@ -85,57 +89,98 @@ func runWeb(ctx context.Context, args []string) error {
|
|||||||
return http.ListenAndServe(webArgs.listen, http.HandlerFunc(webHandler))
|
return http.ListenAndServe(webArgs.listen, http.HandlerFunc(webHandler))
|
||||||
}
|
}
|
||||||
|
|
||||||
// authorize checks whether the provided user has access to the web UI.
|
// authorize returns the name of the user accessing the web UI after verifying
|
||||||
func authorize(name string) error {
|
// whether the user has access to the web UI. The function will write the
|
||||||
if distro.Get() == distro.Synology {
|
// error to the provided http.ResponseWriter.
|
||||||
return authorizeSynology(name)
|
// Note: This is different from a tailscale user, and is typically the local
|
||||||
|
// user on the node.
|
||||||
|
func authorize(w http.ResponseWriter, r *http.Request) (string, error) {
|
||||||
|
switch distro.Get() {
|
||||||
|
case distro.Synology:
|
||||||
|
user, err := synoAuthn()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if err := authorizeSynology(user); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusForbidden)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return user, nil
|
||||||
|
case distro.QNAP:
|
||||||
|
user, resp, err := qnapAuthn(r)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if resp.IsAdmin == 0 {
|
||||||
|
http.Error(w, err.Error(), http.StatusForbidden)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return user, nil
|
||||||
}
|
}
|
||||||
return nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// authorizeSynology checks whether the provided user has access to the web UI
|
// authorizeSynology checks whether the provided user has access to the web UI
|
||||||
// by consulting the membership of the "administrators" group.
|
// by consulting the membership of the "administrators" group.
|
||||||
func authorizeSynology(name string) error {
|
func authorizeSynology(name string) error {
|
||||||
f, err := os.Open("/etc/group")
|
yes, err := groupmember.IsMemberOfGroup("administrators", name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer f.Close()
|
if !yes {
|
||||||
s := bufio.NewScanner(f)
|
return fmt.Errorf("not a member of administrators group")
|
||||||
var agLine string
|
|
||||||
for s.Scan() {
|
|
||||||
if !mem.HasPrefix(mem.B(s.Bytes()), mem.S("administrators:")) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
agLine = s.Text()
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
if err := s.Err(); err != nil {
|
return nil
|
||||||
return err
|
|
||||||
}
|
|
||||||
if agLine == "" {
|
|
||||||
return fmt.Errorf("admin group not defined")
|
|
||||||
}
|
|
||||||
agEntry := strings.Split(agLine, ":")
|
|
||||||
if len(agEntry) < 4 {
|
|
||||||
return fmt.Errorf("malformed admin group entry")
|
|
||||||
}
|
|
||||||
agMembers := agEntry[3]
|
|
||||||
for _, m := range strings.Split(agMembers, ",") {
|
|
||||||
if m == name {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fmt.Errorf("not a member of administrators group")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// authenticate returns the name of the user accessing the web UI.
|
type qnapAuthResponse struct {
|
||||||
// Note: This is different from a tailscale user, and is typically the local
|
AuthPassed int `xml:"authPassed"`
|
||||||
// user on the node.
|
IsAdmin int `xml:"isAdmin"`
|
||||||
func authenticate() (string, error) {
|
AuthSID string `xml:"authSid"`
|
||||||
if distro.Get() != distro.Synology {
|
ErrorValue int `xml:"errorValue"`
|
||||||
return "", nil
|
}
|
||||||
|
|
||||||
|
func qnapAuthn(r *http.Request) (string, *qnapAuthResponse, error) {
|
||||||
|
user, err := r.Cookie("NAS_USER")
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
}
|
}
|
||||||
|
token, err := r.Cookie("qtoken")
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
query := url.Values{
|
||||||
|
"qtoken": []string{token.Value},
|
||||||
|
"user": []string{user.Value},
|
||||||
|
}
|
||||||
|
u := url.URL{
|
||||||
|
Scheme: r.URL.Scheme,
|
||||||
|
Host: r.URL.Host,
|
||||||
|
Path: "/cgi-bin/authLogin.cgi",
|
||||||
|
RawQuery: query.Encode(),
|
||||||
|
}
|
||||||
|
resp, err := http.Get(u.String())
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
out, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
authResp := &qnapAuthResponse{}
|
||||||
|
if err := xml.Unmarshal(out, authResp); err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
if authResp.AuthPassed == 0 {
|
||||||
|
return "", nil, fmt.Errorf("not authenticated")
|
||||||
|
}
|
||||||
|
return user.Value, authResp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func synoAuthn() (string, error) {
|
||||||
cmd := exec.Command("/usr/syno/synoman/webman/modules/authenticate.cgi")
|
cmd := exec.Command("/usr/syno/synoman/webman/modules/authenticate.cgi")
|
||||||
out, err := cmd.CombinedOutput()
|
out, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -144,10 +189,14 @@ func authenticate() (string, error) {
|
|||||||
return strings.TrimSpace(string(out)), nil
|
return strings.TrimSpace(string(out)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func synoTokenRedirect(w http.ResponseWriter, r *http.Request) bool {
|
func authRedirect(w http.ResponseWriter, r *http.Request) bool {
|
||||||
if distro.Get() != distro.Synology {
|
if distro.Get() == distro.Synology {
|
||||||
return false
|
return synoTokenRedirect(w, r)
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func synoTokenRedirect(w http.ResponseWriter, r *http.Request) bool {
|
||||||
if r.Header.Get("X-Syno-Token") != "" {
|
if r.Header.Get("X-Syno-Token") != "" {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -181,80 +230,13 @@ func synoTokenRedirect(w http.ResponseWriter, r *http.Request) bool {
|
|||||||
</body></html>
|
</body></html>
|
||||||
`
|
`
|
||||||
|
|
||||||
const authenticationRedirectHTML = `
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Redirecting...</title>
|
|
||||||
<style>
|
|
||||||
html,
|
|
||||||
body {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
|
||||||
background-color: rgb(249, 247, 246);
|
|
||||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
|
||||||
line-height: 1.5;
|
|
||||||
-webkit-text-size-adjust: 100%;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spinner {
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
border: 4px rgba(112, 110, 109, 0.5) solid;
|
|
||||||
border-left-color: transparent;
|
|
||||||
border-radius: 9999px;
|
|
||||||
width: 4rem;
|
|
||||||
height: 4rem;
|
|
||||||
-webkit-animation: spin 700ms linear infinite;
|
|
||||||
animation: spin 800ms linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
|
||||||
color: rgb(112, 110, 109);
|
|
||||||
padding-left: 0.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@-webkit-keyframes spin {
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="spinner"></div>
|
|
||||||
<div class="label">Redirecting...</div>
|
|
||||||
</body>
|
|
||||||
`
|
|
||||||
|
|
||||||
func webHandler(w http.ResponseWriter, r *http.Request) {
|
func webHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
if synoTokenRedirect(w, r) {
|
if authRedirect(w, r) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := authenticate()
|
user, err := authorize(w, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := authorize(user); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusForbidden)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -268,7 +250,7 @@ type mi map[string]interface{
|
|||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
url, err := tailscaleUpForceReauth(r.Context())
|
url, err := tailscaleUpForceReauth(r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(500)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
json.NewEncoder(w).Encode(mi{"error": err.Error()})
|
json.NewEncoder(w).Encode(mi{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -278,7 +260,7 @@ type mi map[string]interface{
|
|||||||
|
|
||||||
st, err := tailscale.Status(r.Context())
|
st, err := tailscale.Status(r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), 500)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -296,7 +278,7 @@ type mi map[string]interface{
|
|||||||
|
|
||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
if err := tmpl.Execute(buf, data); err != nil {
|
if err := tmpl.Execute(buf, data); err != nil {
|
||||||
http.Error(w, err.Error(), 500)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Write(buf.Bytes())
|
w.Write(buf.Bytes())
|
||||||
|
@ -53,6 +53,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
|||||||
tailscale.com/types/wgkey from tailscale.com/types/netmap+
|
tailscale.com/types/wgkey from tailscale.com/types/netmap+
|
||||||
tailscale.com/util/dnsname from tailscale.com/cmd/tailscale/cli+
|
tailscale.com/util/dnsname from tailscale.com/cmd/tailscale/cli+
|
||||||
W tailscale.com/util/endian from tailscale.com/net/netns
|
W tailscale.com/util/endian from tailscale.com/net/netns
|
||||||
|
tailscale.com/util/groupmember from tailscale.com/cmd/tailscale/cli
|
||||||
L tailscale.com/util/lineread from tailscale.com/net/interfaces
|
L tailscale.com/util/lineread from tailscale.com/net/interfaces
|
||||||
tailscale.com/version from tailscale.com/cmd/tailscale/cli+
|
tailscale.com/version from tailscale.com/cmd/tailscale/cli+
|
||||||
tailscale.com/version/distro from tailscale.com/cmd/tailscale/cli+
|
tailscale.com/version/distro from tailscale.com/cmd/tailscale/cli+
|
||||||
@ -118,13 +119,14 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
|||||||
debug/macho from rsc.io/goversion/version
|
debug/macho from rsc.io/goversion/version
|
||||||
debug/pe from rsc.io/goversion/version
|
debug/pe from rsc.io/goversion/version
|
||||||
embed from tailscale.com/cmd/tailscale/cli
|
embed from tailscale.com/cmd/tailscale/cli
|
||||||
encoding from encoding/json
|
encoding from encoding/json+
|
||||||
encoding/asn1 from crypto/x509+
|
encoding/asn1 from crypto/x509+
|
||||||
encoding/base64 from encoding/json+
|
encoding/base64 from encoding/json+
|
||||||
encoding/binary from compress/gzip+
|
encoding/binary from compress/gzip+
|
||||||
encoding/hex from crypto/x509+
|
encoding/hex from crypto/x509+
|
||||||
encoding/json from expvar+
|
encoding/json from expvar+
|
||||||
encoding/pem from crypto/tls+
|
encoding/pem from crypto/tls+
|
||||||
|
encoding/xml from tailscale.com/cmd/tailscale/cli
|
||||||
errors from bufio+
|
errors from bufio+
|
||||||
expvar from tailscale.com/derp+
|
expvar from tailscale.com/derp+
|
||||||
flag from github.com/peterbourgon/ff/v2+
|
flag from github.com/peterbourgon/ff/v2+
|
||||||
@ -156,6 +158,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
|||||||
os from crypto/rand+
|
os from crypto/rand+
|
||||||
os/exec from github.com/toqueteos/webbrowser+
|
os/exec from github.com/toqueteos/webbrowser+
|
||||||
os/signal from tailscale.com/cmd/tailscale/cli
|
os/signal from tailscale.com/cmd/tailscale/cli
|
||||||
|
os/user from tailscale.com/util/groupmember
|
||||||
path from debug/dwarf+
|
path from debug/dwarf+
|
||||||
path/filepath from crypto/x509+
|
path/filepath from crypto/x509+
|
||||||
reflect from crypto/x509+
|
reflect from crypto/x509+
|
||||||
|
@ -135,6 +135,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
|||||||
L tailscale.com/util/cmpver from tailscale.com/net/dns
|
L tailscale.com/util/cmpver from tailscale.com/net/dns
|
||||||
tailscale.com/util/dnsname from tailscale.com/ipn/ipnstate+
|
tailscale.com/util/dnsname from tailscale.com/ipn/ipnstate+
|
||||||
LW tailscale.com/util/endian from tailscale.com/net/netns+
|
LW tailscale.com/util/endian from tailscale.com/net/netns+
|
||||||
|
tailscale.com/util/groupmember from tailscale.com/ipn/ipnserver
|
||||||
L tailscale.com/util/lineread from tailscale.com/control/controlclient+
|
L tailscale.com/util/lineread from tailscale.com/control/controlclient+
|
||||||
tailscale.com/util/osshare from tailscale.com/cmd/tailscaled+
|
tailscale.com/util/osshare from tailscale.com/cmd/tailscaled+
|
||||||
tailscale.com/util/pidowner from tailscale.com/ipn/ipnserver
|
tailscale.com/util/pidowner from tailscale.com/ipn/ipnserver
|
||||||
|
@ -24,7 +24,6 @@
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -41,9 +40,11 @@
|
|||||||
"tailscale.com/safesocket"
|
"tailscale.com/safesocket"
|
||||||
"tailscale.com/smallzstd"
|
"tailscale.com/smallzstd"
|
||||||
"tailscale.com/types/logger"
|
"tailscale.com/types/logger"
|
||||||
|
"tailscale.com/util/groupmember"
|
||||||
"tailscale.com/util/pidowner"
|
"tailscale.com/util/pidowner"
|
||||||
"tailscale.com/util/systemd"
|
"tailscale.com/util/systemd"
|
||||||
"tailscale.com/version"
|
"tailscale.com/version"
|
||||||
|
"tailscale.com/version/distro"
|
||||||
"tailscale.com/wgengine"
|
"tailscale.com/wgengine"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -347,51 +348,32 @@ func isReadonlyConn(ci connIdentity, operatorUID string, logf logger.Logf) bool
|
|||||||
logf("connection from userid %v; is configured operator", uid)
|
logf("connection from userid %v; is configured operator", uid)
|
||||||
return rw
|
return rw
|
||||||
}
|
}
|
||||||
var adminGroupID string
|
if yes, err := isLocalAdmin(uid); err != nil {
|
||||||
switch runtime.GOOS {
|
logf("connection from userid %v; read-only; %v", uid, err)
|
||||||
case "darwin":
|
|
||||||
adminGroupID = darwinAdminGroupID()
|
|
||||||
default:
|
|
||||||
logf("connection from userid %v; read-only", uid)
|
|
||||||
return ro
|
return ro
|
||||||
}
|
} else if yes {
|
||||||
if adminGroupID == "" {
|
logf("connection from userid %v; is local admin, has access", uid)
|
||||||
logf("connection from userid %v; no system admin group found, read-only", uid)
|
return rw
|
||||||
return ro
|
|
||||||
}
|
|
||||||
u, err := user.LookupId(uid)
|
|
||||||
if err != nil {
|
|
||||||
logf("connection from userid %v; failed to look up user; read-only", uid)
|
|
||||||
return ro
|
|
||||||
}
|
|
||||||
gids, err := u.GroupIds()
|
|
||||||
if err != nil {
|
|
||||||
logf("connection from userid %v; failed to look up groups; read-only", uid)
|
|
||||||
return ro
|
|
||||||
}
|
|
||||||
for _, gid := range gids {
|
|
||||||
if gid == adminGroupID {
|
|
||||||
logf("connection from userid %v; is local admin, has access", uid)
|
|
||||||
return rw
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
logf("connection from userid %v; read-only", uid)
|
logf("connection from userid %v; read-only", uid)
|
||||||
return ro
|
return ro
|
||||||
}
|
}
|
||||||
|
|
||||||
var darwinAdminGroupIDCache atomic.Value // of string
|
func isLocalAdmin(uid string) (bool, error) {
|
||||||
|
u, err := user.LookupId(uid)
|
||||||
func darwinAdminGroupID() string {
|
|
||||||
s, _ := darwinAdminGroupIDCache.Load().(string)
|
|
||||||
if s != "" {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
g, err := user.LookupGroup("admin")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ""
|
return false, err
|
||||||
}
|
}
|
||||||
darwinAdminGroupIDCache.Store(g.Gid)
|
var adminGroup string
|
||||||
return g.Gid
|
switch {
|
||||||
|
case runtime.GOOS == "darwin":
|
||||||
|
adminGroup = "admin"
|
||||||
|
case distro.Get() == distro.QNAP:
|
||||||
|
adminGroup = "administrators"
|
||||||
|
default:
|
||||||
|
return false, fmt.Errorf("no system admin group found")
|
||||||
|
}
|
||||||
|
return groupmember.IsMemberOfGroup(adminGroup, u.Username)
|
||||||
}
|
}
|
||||||
|
|
||||||
// inUseOtherUserError is the error type for when the server is in use
|
// inUseOtherUserError is the error type for when the server is in use
|
||||||
|
21
util/groupmember/groupmember.go
Normal file
21
util/groupmember/groupmember.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Package groupmemeber verifies group membership of the provided user on the
|
||||||
|
// local system.
|
||||||
|
package groupmember
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrNotImplemented = errors.New("not implemented for GOOS=" + runtime.GOOS)
|
||||||
|
|
||||||
|
// IsMemberOfGroup verifies if the provided user is member of the provided
|
||||||
|
// system group.
|
||||||
|
// If verfication fails, an error is returned.
|
||||||
|
func IsMemberOfGroup(group, userName string) (bool, error) {
|
||||||
|
return isMemberOfGroup(group, userName)
|
||||||
|
}
|
48
util/groupmember/groupmember_cgo.go
Normal file
48
util/groupmember/groupmember_cgo.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build cgo
|
||||||
|
|
||||||
|
package groupmember
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/user"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
func isMemberOfGroup(group, name string) (bool, error) {
|
||||||
|
u, err := user.Lookup(name)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
ugids, err := u.GroupIds()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
gid, err := getGroupID(group)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
for _, ugid := range ugids {
|
||||||
|
if gid == ugid {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var groupIDCache sync.Map // of string
|
||||||
|
|
||||||
|
func getGroupID(groupName string) (string, error) {
|
||||||
|
s, ok := groupIDCache.Load(groupName)
|
||||||
|
if ok {
|
||||||
|
return s.(string), nil
|
||||||
|
}
|
||||||
|
g, err := user.LookupGroup(groupName)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
groupIDCache.Store(groupName, g.Gid)
|
||||||
|
return g.Gid, nil
|
||||||
|
}
|
9
util/groupmember/groupmember_noimpl.go
Normal file
9
util/groupmember/groupmember_noimpl.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build !cgo,!linux,!darwin
|
||||||
|
|
||||||
|
package groupmember
|
||||||
|
|
||||||
|
func isMemberOfGroup(group, name string) (bool, error) { return false, ErrNotImplemented }
|
71
util/groupmember/groupmember_notcgo.go
Normal file
71
util/groupmember/groupmember_notcgo.go
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build !cgo
|
||||||
|
// +build linux darwin
|
||||||
|
|
||||||
|
package groupmember
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"go4.org/mem"
|
||||||
|
"tailscale.com/version/distro"
|
||||||
|
)
|
||||||
|
|
||||||
|
func isMemberOfGroup(group, name string) (bool, error) {
|
||||||
|
if distro.Get() == distro.Synology {
|
||||||
|
return isMemberOfGroupEtcGroup(group, name)
|
||||||
|
}
|
||||||
|
cmd := exec.Command("/usr/bin/env", "groups", name)
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
groups := strings.Split(strings.TrimSpace(string(out)), " ")
|
||||||
|
for _, g := range groups {
|
||||||
|
if g == group {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isMemberOfGroupEtcGroup(group, name string) (bool, error) {
|
||||||
|
f, err := os.Open("/etc/group")
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
s := bufio.NewScanner(f)
|
||||||
|
var agLine string
|
||||||
|
for s.Scan() {
|
||||||
|
if !mem.HasPrefix(mem.B(s.Bytes()), mem.S(fmt.Sprintf("%s:", group))) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
agLine = s.Text()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err := s.Err(); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if agLine == "" {
|
||||||
|
return false, fmt.Errorf("admin group not defined")
|
||||||
|
}
|
||||||
|
agEntry := strings.Split(agLine, ":")
|
||||||
|
if len(agEntry) < 4 {
|
||||||
|
return false, fmt.Errorf("malformed admin group entry")
|
||||||
|
}
|
||||||
|
agMembers := agEntry[3]
|
||||||
|
for _, m := range strings.Split(agMembers, ",") {
|
||||||
|
if m == name {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
@ -18,6 +18,7 @@
|
|||||||
Synology = Distro("synology")
|
Synology = Distro("synology")
|
||||||
OpenWrt = Distro("openwrt")
|
OpenWrt = Distro("openwrt")
|
||||||
NixOS = Distro("nixos")
|
NixOS = Distro("nixos")
|
||||||
|
QNAP = Distro("qnap")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Get returns the current distro, or the empty string if unknown.
|
// Get returns the current distro, or the empty string if unknown.
|
||||||
@ -50,6 +51,8 @@ func linuxDistro() Distro {
|
|||||||
return OpenWrt
|
return OpenWrt
|
||||||
case have("/run/current-system/sw/bin/nixos-version"):
|
case have("/run/current-system/sw/bin/nixos-version"):
|
||||||
return NixOS
|
return NixOS
|
||||||
|
case have("/etc/config/uLinux.conf"):
|
||||||
|
return QNAP
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user