Skip to content

Commit

Permalink
✨ feat: implements APIs for managing ACLs
Browse files Browse the repository at this point in the history
Headscale currently lacks the APIs to manange the ACLs. The only way
possible currently is to load the ACLs via file and changes to the
policy requires reloading the headscale process. This also makes it
difficult to integrate your headscale via APIs with no ACL management.

This commit introduces two APIs that allow your to get and set the
policy.
  • Loading branch information
pallabpain committed Mar 4, 2024
1 parent a244eab commit 39b8afa
Show file tree
Hide file tree
Showing 27 changed files with 1,673 additions and 408 deletions.
96 changes: 96 additions & 0 deletions cmd/headscale/cli/acl.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package cli

import (
"encoding/json"
"os"

"github.com/rs/zerolog/log"
"github.com/spf13/cobra"

v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/juanfont/headscale/hscontrol/types"
)

func init() {
rootCmd.AddCommand(aclCmd)
aclCmd.AddCommand(getACL)

setACL.Flags().StringP("policy", "p", "", "Path to a policy file in JSON format")
if err := setACL.MarkFlagRequired("policy"); err != nil {
log.Fatal().Err(err).Msg("")
}
aclCmd.AddCommand(setACL)
}

var aclCmd = &cobra.Command{
Use: "acl",
Short: "Manage the Headscale ACL Policy",
Aliases: []string{"acl"},
}

var getACL = &cobra.Command{
Use: "get",
Short: "Print the current ACL Policy JSON",
Aliases: []string{"show", "view", "fetch"},
Run: func(cmd *cobra.Command, args []string) {
ctx, client, conn, cancel := getHeadscaleCLIClient()
defer cancel()
defer conn.Close()

request := &v1.GetACLRequest{}

response, err := client.GetACL(ctx, request)
if err != nil {
log.Fatal().Err(err).Msg("Cannot get ACL Policy")

return
}

SuccessOutput(response.GetPolicy(), "", "json")
},
}

var setACL = &cobra.Command{
Use: "set",
Short: "Updates the ACL Policy",
Long: `
Updates the existing ACL Policy with the provided policy. The policy must be a valid JSON object.
This command only works when the acl.policy_mode is set to "db", and the policy will be stored in the database.`,
Aliases: []string{"put", "update"},
Run: func(cmd *cobra.Command, args []string) {
policyPath, _ := cmd.Flags().GetString("policy")

f, err := os.Open(policyPath)
if err != nil {
log.Fatal().Err(err).Msg("Error opening the policy file")

return
}

decoder := json.NewDecoder(f)

var polBytes []byte
if err := decoder.Decode(&polBytes); err != nil {
log.Fatal().Err(err).Msg("Error decoding the policy file")

return
}

acl := types.ACL{Policy: polBytes}

request := &v1.SetACLRequest{Policy: acl.Proto().GetPolicy()}

ctx, client, conn, cancel := getHeadscaleCLIClient()
defer cancel()
defer conn.Close()

response, err := client.SetACL(ctx, request)
if err != nil {
log.Fatal().Err(err).Msg("Failed to set ACL Policy")

return
}

SuccessOutput(response.GetPolicy(), "", output)
},
}
19 changes: 10 additions & 9 deletions cmd/headscale/cli/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,17 @@ import (
"os"
"reflect"

v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/juanfont/headscale/hscontrol"
"github.com/juanfont/headscale/hscontrol/policy"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/hscontrol/util"
"github.com/rs/zerolog/log"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
"gopkg.in/yaml.v3"

v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/juanfont/headscale/hscontrol"
"github.com/juanfont/headscale/hscontrol/policy"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/hscontrol/util"
)

const (
Expand All @@ -41,7 +42,7 @@ func getHeadscaleApp() (*hscontrol.Headscale, error) {

// We are doing this here, as in the future could be cool to have it also hot-reload

if cfg.ACL.PolicyPath != "" {
if cfg.ACL.PolicyMode == types.ACLPolicyModeFile && cfg.ACL.PolicyPath != "" {
aclPath := util.AbsolutePathFromConfigPath(cfg.ACL.PolicyPath)
pol, err := policy.LoadACLPolicyFromPath(aclPath)
if err != nil {
Expand Down Expand Up @@ -89,7 +90,7 @@ func getHeadscaleCLIClient() (context.Context, v1.HeadscaleServiceClient, *grpc.

// Try to give the user better feedback if we cannot write to the headscale
// socket.
socket, err := os.OpenFile(cfg.UnixSocket, os.O_WRONLY, SocketWritePermissions) //nolint
socket, err := os.OpenFile(cfg.UnixSocket, os.O_WRONLY, SocketWritePermissions) // nolint
if err != nil {
if os.IsPermission(err) {
log.Fatal().
Expand Down Expand Up @@ -167,13 +168,13 @@ func SuccessOutput(result interface{}, override string, outputFormat string) {
log.Fatal().Err(err).Msg("failed to unmarshal output")
}
default:
//nolint
// nolint
fmt.Println(override)

return
}

//nolint
// nolint
fmt.Println(string(jsonBytes))
}

Expand Down
4 changes: 3 additions & 1 deletion config-example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,9 @@ log:
# Path to a file containg ACL policies.
# ACLs can be defined as YAML or HUJSON.
# https://tailscale.com/kb/1018/acls/
acl_policy_path: ""
acl:
policy_path: ""
policy_mode: "db"

## DNS
#
Expand Down
Loading

0 comments on commit 39b8afa

Please sign in to comment.