Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implements apis for managing headscale policy #1792

Merged
merged 2 commits into from
Jul 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/test-integration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ jobs:
- TestACLNamedHostsCanReachBySubnet
- TestACLNamedHostsCanReach
- TestACLDevice1CanAccessDevice2
- TestPolicyUpdateWhileRunningWithCLIInDatabase
- TestOIDCAuthenticationPingAll
- TestOIDCExpireNodesBasedOnTokenExpiry
- TestAuthWebFlowAuthenticationPingAll
Expand All @@ -35,6 +36,7 @@ jobs:
- TestNodeExpireCommand
- TestNodeRenameCommand
- TestNodeMoveCommand
- TestPolicyCommand
- TestDERPServerScenario
- TestPingAllByIP
- TestPingAllByIPPublicDERP
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ after improving the test harness as part of adopting [#1460](https://github.com/
- This is in preperation to fix Headscales implementation of tags which currently does not correctly remove the link between a tagged device and a user. As tagged devices will not have a user, this will require a change to the DNS generation, removing the username, see [#1369](https://github.com/juanfont/headscale/issues/1369) for more information.
- `use_username_in_magic_dns` can be used to turn this behaviour on again, but note that this option _will be removed_ when tags are fixed.
- This option brings Headscales behaviour in line with Tailscale.
- YAML files are no longer supported for headscale policy. [#1792](https://github.com/juanfont/headscale/pull/1792)
- HuJSON is now the only supported format for policy.

### Changes

Expand All @@ -64,6 +66,7 @@ after improving the test harness as part of adopting [#1460](https://github.com/
- Restore foreign keys and add constraints [#1562](https://github.com/juanfont/headscale/pull/1562)
- Make registration page easier to use on mobile devices
- Make write-ahead-log default on and configurable for SQLite [#1985](https://github.com/juanfont/headscale/pull/1985)
- Add APIs for managing headscale policy. [#1792](https://github.com/juanfont/headscale/pull/1792)

## 0.22.3 (2023-05-12)

Expand Down
91 changes: 91 additions & 0 deletions cmd/headscale/cli/policy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package cli

import (
"io"
"os"

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

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

func init() {
rootCmd.AddCommand(policyCmd)
policyCmd.AddCommand(getPolicy)

setPolicy.Flags().StringP("file", "f", "", "Path to a policy file in HuJSON format")
if err := setPolicy.MarkFlagRequired("file"); err != nil {
log.Fatal().Err(err).Msg("")
}
policyCmd.AddCommand(setPolicy)
}

var policyCmd = &cobra.Command{
Use: "policy",
Short: "Manage the Headscale ACL Policy",
}

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

request := &v1.GetPolicyRequest{}

response, err := client.GetPolicy(ctx, request)
if err != nil {
log.Fatal().Err(err).Msg("Failed to get the policy")

return
}

// TODO(pallabpain): Maybe print this better?
SuccessOutput("", response.GetPolicy(), "hujson")
},
}

var setPolicy = &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 HuJSON 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("file")

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

return
}
defer f.Close()

policyBytes, err := io.ReadAll(f)
if err != nil {
log.Fatal().Err(err).Msg("Error reading the policy file")

return
}

request := &v1.SetPolicyRequest{Policy: string(policyBytes)}

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

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

return
}

SuccessOutput(nil, "Policy updated.", "")
},
}
31 changes: 8 additions & 23 deletions cmd/headscale/cli/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,16 @@ 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/types"
"github.com/juanfont/headscale/hscontrol/util"
)

const (
Expand All @@ -39,21 +39,6 @@ func getHeadscaleApp() (*hscontrol.Headscale, error) {
return nil, err
}

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

if cfg.ACL.PolicyPath != "" {
aclPath := util.AbsolutePathFromConfigPath(cfg.ACL.PolicyPath)
pol, err := policy.LoadACLPolicyFromPath(aclPath)
if err != nil {
log.Fatal().
Str("path", aclPath).
Err(err).
Msg("Could not load the ACL policy")
}

app.ACLPolicy = pol
}

return app, nil
}

Expand Down Expand Up @@ -89,7 +74,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 +152,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
16 changes: 12 additions & 4 deletions config-example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -203,10 +203,18 @@ log:
format: text
level: info

# Path to a file containing ACL policies.
# ACLs can be defined as YAML or HUJSON.
# https://tailscale.com/kb/1018/acls/
acl_policy_path: ""
## Policy
# headscale supports Tailscale's ACL policies.
# Please have a look to their KB to better
# understand the concepts: https://tailscale.com/kb/1018/acls/
policy:
# The mode can be "file" or "database" that defines
# where the ACL policies are stored and read from.
mode: file
# If the mode is set to "file", the
# path to a file containing ACL policies.
# The file can be in YAML or HuJSON format.
path: ""

## DNS
#
Expand Down
2 changes: 1 addition & 1 deletion gen/go/headscale/v1/apikey.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion gen/go/headscale/v1/device.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading