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 ACLs #1792

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

pallabpain
Copy link
Contributor

@pallabpain pallabpain commented Feb 24, 2024

Description

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.

APIs

Set ACL Policy

PUT /api/v1/acl

Payload (Example only)

{"policy": {"groups": {}, "tags": {}}}

Get ACL Policy

GET /api/v1/acl

Response

{"policy": {"groups": {}, "tags": {}}}

CLI

→ go run cmd/headscale/headscale.go --config ./config.yaml acl --help
Manage the Headscale ACL Policy

Usage:
  headscale acl [command]

Available Commands:
  get         Print the current ACL Policy JSON
  set         Updates the ACL Policy

Flags:
  -h, --help   help for acl

Global Flags:
  -c, --config string   config file (default is /etc/headscale/config.yaml)
      --force           Disable prompts and forces the execution
  -o, --output string   Output format. Empty for human-readable, 'json', 'json-line' or 'yaml'

Use "headscale acl [command] --help" for more information about a command.

Testing

acls_demo.mp4
  • read the CONTRIBUTING guidelines
  • raised a GitHub issue or discussed it on the projects chat beforehand
  • added unit tests
  • added integration tests
  • updated documentation if needed
  • updated CHANGELOG.md

Resolves

hscontrol/app.go Outdated Show resolved Hide resolved
@pallabpain pallabpain marked this pull request as ready for review March 5, 2024 18:14
@pallabpain
Copy link
Contributor Author

@evenh @kradalby Please take a look. 🙇🏼

@evenh
Copy link
Contributor

evenh commented Mar 6, 2024

I might have some free time this evening to do a review

@pallabpain
Copy link
Contributor Author

I've attached a screen recording to the PR in case someone wishes to see how the APIs work.

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.
Copy link
Contributor

@evenh evenh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall it looks good - I've left some inline comments. One open question is whether to support reading the current ACL via the new APIs/CLI commands even if the policy is backed by a file (for convenience)?

Policy datatypes.JSON

CreatedAt *time.Time
Expiration *time.Time
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is Expiration and LastSeen copy pasta? :-)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. 😅 I'll fix this.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GORM also adds created at I believe, so that can also be omitted

@@ -180,6 +189,8 @@ func LoadConfig(path string, isFile bool) error {
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.AutomaticEnv()

viper.SetDefault("acl.policy_mode", ACLPolicyModeDB)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't this break existing setups which already uses a file? I'm not a maintainer, but I would be fine with the DB being the default going forward but in that case we should not break existing users setups.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this will be a breaking change. I can set up an alias for existing users. However, their setups may already be broken from the recent database config restructure. I'm of the opinion that we need not keep it backward compatible since users will have to update their configs anyway.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comment earlier, I think file should still be the default. I'm open to be convinced.

return nil, types.ErrACLPolicyUpdateIsDisabled
}

polBytes, err := request.GetPolicy().MarshalJSON()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this fail if the input is HuJSON (default Tailscale ACL format)? policy.LoadACLPolicyFromBytes will use the hujson parser by default when the format is not yaml.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for bringing this up. I will test and verify it.

return nil, errors.Wrap(err, types.ErrInvalidACLPolicyFormat.Error())
}

api.h.ACLPolicy = a
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this line be moved down to after the ACL is successfully persisted?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. I've moved it after we set the ACL in the database.

@@ -761,25 +768,9 @@ func (h *Headscale) Serve() error {
Msg("Received SIGHUP, reloading ACL and Config")

// TODO(kradalby): Reload config on SIGHUP
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// TODO(kradalby): Reload config on SIGHUP

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still isn't implemented. I'll keep this for the time being.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should probably not allow "large" TODOs like this without an issue... (including me)

hscontrol/app.go Outdated
h.ACLPolicy = pol

log.Info().
Msg("ACL policy successfully reloaded, notifying nodes of change")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Msg("ACL policy successfully reloaded, notifying nodes of change")
Msg("ACL policy successfully loaded, notifying nodes of change")

This function is both used for the initial loading and for the SIGHUP reloading.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved the log close to the notify call.

hscontrol/app.go Outdated
}

if h.ACLPolicy != nil {
ctx := types.NotifyCtx(context.Background(), "acl-sighup", "na")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to notifyCtx + notifyAll if it's not a reload, but a normal startup? 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do not. 😅 Thanks, I've updated this bit.

@pallabpain
Copy link
Contributor Author

Overall it looks good - I've left some inline comments. One open question is whether to support reading the current ACL via the new APIs/CLI commands even if the policy is backed by a file (for convenience)?

Enabled it. 👍🏼 Now it will work for both modes.

Copy link
Collaborator

@kradalby kradalby left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this looks reasonable, some comments and questions and to larger things:

I am tempted to use this opportunity to change the general phrasing from ACL to Policy through out the code, particularly since we are changing the config, adding commands etc.

I think the base format for the policy should be HuJSON, it is a bit more flexible format that allows preserving comments. This probably means using string/byte for the database and not the JSON type, but we dont really use it (or will use SQL to look up in the JSON) so that should not be a problem.
As part of this, I am willing to discuss dropping the YAML support to just streamline what we support.


var getACL = &cobra.Command{
Use: "get",
Short: "Print the current ACL Policy JSON",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A very high level note, I think this should support the same HuJSON (and potentially YAML) format that the ACL support.

The main reason for this is that it can preserve comments which would be important for large policies.

I would also assume that some of the WebUIs will implement some sort of editors, and having comments would be important for documentation purposes.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the record, I am OK with just HuJSON, we might want to remove the YAML support and only support one format.

# If "file" is selected, the policies will be loaded from a file
# located at `policy_path`. It will not be stored in the databse
# and you will not be able to get or set the ACL via the ACL APIs.
policy_mode: "db"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm open for the example config to be db, but the default should still be file.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm open for the example config to be db, but the default should still be file.

Personally I strongly disagree.

  1. Many people still use docker. (I know I know, it's not officially supported... still!)
    Manipulating files inside it, especially if the container is not running is a very hard task.

  2. IMHO it is more secure to store ACL data inside a db. A separate file can be easier hacked / modified / viewed.
    After all, ACL is the "main security" config of HeadScale, telling who has access to what !
    Please keep in mind: The main reason for using VPNs is security.
    Offering an easy-to-view / edit file to the open is against it.

  3. Eventually more people will use this feature using a web-UI, so it would make much easier to setup with that already.

  4. For those who like to do things manually (by typing text files, etc) -> changing the config to file will not be a big task to do, but for those who are not used to do tasks like this: it would be an "impossible nightmare".

Anyway: is the database password protected by default, so only HS can access it?

  • or can other programs access it too?

@@ -96,6 +98,7 @@ require (
github.com/glebarez/go-sqlite v1.22.0 // indirect
github.com/go-jose/go-jose/v3 v3.0.1 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-sql-driver/mysql v1.7.0 // indirect
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wonder why we suddenly need a mysql driver?

@@ -761,25 +768,9 @@ func (h *Headscale) Serve() error {
Msg("Received SIGHUP, reloading ACL and Config")

// TODO(kradalby): Reload config on SIGHUP
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should probably not allow "large" TODOs like this without an issue... (including me)

aclPath := util.AbsolutePathFromConfigPath(h.cfg.ACL.PolicyPath)
pol, err := policy.LoadACLPolicyFromPath(aclPath)
if err != nil {
return pkgerrors.Wrap(err, "failed to load ACL policy from file")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whats the difference between this error wrap compared to:

fmt.Errorf("failed to load ACL policy from file: %w", err)

Policy datatypes.JSON

CreatedAt *time.Time
Expiration *time.Time
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GORM also adds created at I believe, so that can also be omitted

@@ -180,6 +189,8 @@ func LoadConfig(path string, isFile bool) error {
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.AutomaticEnv()

viper.SetDefault("acl.policy_mode", ACLPolicyModeDB)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comment earlier, I think file should still be the default. I'm open to be convinced.

Comment on lines +390 to +391
policyPath := viper.GetString("acl.policy_path")
policyMode := viper.GetString("acl.policy_mode")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should just use the term policy?

policy:
  mode: "" // <-- does "source" or "storage" or "backend" make more sense than mode?
  path: ""

)

// ACL describes the data model for ACLs used to restrict access to resources.
type ACL struct {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was a proposal of having the policy versioned. I do not think we should do that now, but I think we should think of if there is anything about the format that should be addressed to enable that in the future. I do not have any concerns per now, but want to hear what you think.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

having the policy versioned.

I agree. Having plus one line of a simple version=2024-03-24 is not a big deal now,
but can save us lots of trouble later.

_(In my 30y. of experience I've realized:

  • it is better to use ISO-dates for versions.

Saves lot's of time and prevents confusion.
Also better to merge with release-notes / what changed.)_

@pallabpain
Copy link
Contributor Author

pallabpain commented Mar 7, 2024

I think this looks reasonable, some comments and questions and to larger things:

I am tempted to use this opportunity to change the general phrasing from ACL to Policy through out the code, particularly since we are changing the config, adding commands etc.

I think the base format for the policy should be HuJSON, it is a bit more flexible format that allows preserving comments. This probably means using string/byte for the database and not the JSON type, but we dont really use it (or will use SQL to look up in the JSON) so that should not be a problem. As part of this, I am willing to discuss dropping the YAML support to just streamline what we support.

Thanks for the review. This sounds like a good idea. I wrote this implementation to quickly demonstrate the APIs and commands that we can use as a reference to discuss the feature further.

Keeping the base format as HuJSON should be the way to proceed and we can store it as a bytearray or string in the db since we don't intend to query it any further. Although I'm not entirely sure how the API request and response will look over RPC since I do not want to model the policy structure in the proto definition.

I am also okay with keeping the default mode as file.

Let me update this PR with the suggestions and put it our for a second round of review.

@evenh
Copy link
Contributor

evenh commented Mar 22, 2024

Hi @pallabpain. Have you had a chance to update the PR yet?

@pallabpain
Copy link
Contributor Author

Hi @pallabpain. Have you had a chance to update the PR yet?

I haven't had the chance to update the PR, yet. I have some untested changes locally that I should be able to work on this weekend. I may change the request-response structure based on the newer comments.

@IamTaoChen
Copy link

Maybe, we should create 2 new tables to store groups and tags. And sometimes, we just want to update groups or tags. So /api/v1/acl/groups, /api/v1/acl/tags and /api/v1/acl/all may be more useful.

I'm thinking about how to update ACL groups automatically, e.g. AD/LDAP or extract from OIDC (OIDC may be a problem because it can only get groups when users authenticate)

@kradalby
Copy link
Collaborator

@pallabpain there has been quite some changes, do you think that you might have time to pick this up again?

@pallabpain
Copy link
Contributor Author

@pallabpain there has been quite some changes, do you think that you might have time to pick this up again?

Sure. I'll review the recent changes and update the pull request. I'll try and get a working version ready soon.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants