mirror of
https://github.com/project-zot/zot.git
synced 2026-06-17 21:09:23 +08:00
feat(authz): introduce conditional access control via CEL (#4040)
This commit is contained in:
@@ -792,6 +792,12 @@ func validateAuthzPolicies(config *config.Config, logger zlog.Logger) error {
|
||||
return fmt.Errorf("%w: %s", zerr.ErrBadConfig, msg)
|
||||
}
|
||||
|
||||
if _, err := api.CompileAccessControl(accessControlConfig); err != nil {
|
||||
logger.Error().Err(err).Msg("failed to compile access control policy conditions")
|
||||
|
||||
return fmt.Errorf("%w: %w", zerr.ErrBadConfig, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -87,6 +87,95 @@ func TestServerUsage(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestLoadConfigurationDecodesPolicyConditions(t *testing.T) {
|
||||
Convey("conditions on accessControl policy decode into []Condition", t, func() {
|
||||
htpasswdPath := MakeHtpasswdFileFromString(t, "alice:$2y$05$ajq8Q7fbtFRQvPndnct8OuRu7n6BDpRYHvz7dNH0G9z2j5XbB7yIm")
|
||||
content := fmt.Sprintf(`{
|
||||
"storage": {"rootDirectory": "/tmp/zot"},
|
||||
"http": {
|
||||
"address": "127.0.0.1",
|
||||
"port": "8080",
|
||||
"auth": {"htpasswd": {"path": %q}},
|
||||
"accessControl": {
|
||||
"repositories": {
|
||||
"**": {
|
||||
"policies": [
|
||||
{
|
||||
"users": ["alice"],
|
||||
"actions": ["read"],
|
||||
"conditions": [
|
||||
{
|
||||
"expression": "req.time < timestamp(\"2099-12-31T23:59:59Z\")",
|
||||
"message": "access expired"
|
||||
},
|
||||
{
|
||||
"expression": "req.repository.startsWith(\"prod/\")",
|
||||
"message": "only prod/* allowed"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"users": ["bob"],
|
||||
"actions": ["read"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`, htpasswdPath)
|
||||
|
||||
tmpfile := MakeTempFileWithContent(t, "zot-policy-conditions.json", content)
|
||||
cfg := config.New()
|
||||
|
||||
err := cli.LoadConfiguration(cfg, tmpfile)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
policies := cfg.HTTP.AccessControl.Repositories["**"].Policies
|
||||
So(policies, ShouldHaveLength, 2)
|
||||
So(policies[0].Conditions, ShouldHaveLength, 2)
|
||||
So(policies[0].Conditions[0].Expression, ShouldEqual,
|
||||
`req.time < timestamp("2099-12-31T23:59:59Z")`)
|
||||
So(policies[0].Conditions[0].Message, ShouldEqual, "access expired")
|
||||
So(policies[0].Conditions[1].Expression, ShouldEqual, `req.repository.startsWith("prod/")`)
|
||||
So(policies[0].Conditions[1].Message, ShouldEqual, "only prod/* allowed")
|
||||
So(policies[1].Conditions, ShouldBeEmpty)
|
||||
})
|
||||
|
||||
Convey("malformed condition expression fails config load", t, func() {
|
||||
htpasswdPath := MakeHtpasswdFileFromString(t, "alice:$2y$05$ajq8Q7fbtFRQvPndnct8OuRu7n6BDpRYHvz7dNH0G9z2j5XbB7yIm")
|
||||
content := fmt.Sprintf(`{
|
||||
"storage": {"rootDirectory": "/tmp/zot"},
|
||||
"http": {
|
||||
"address": "127.0.0.1",
|
||||
"port": "8080",
|
||||
"auth": {"htpasswd": {"path": %q}},
|
||||
"accessControl": {
|
||||
"repositories": {
|
||||
"**": {
|
||||
"policies": [
|
||||
{
|
||||
"users": ["alice"],
|
||||
"actions": ["read"],
|
||||
"conditions": [
|
||||
{"expression": "this is not valid CEL", "message": "broken"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`, htpasswdPath)
|
||||
|
||||
tmpfile := MakeTempFileWithContent(t, "zot-policy-conditions-bad.json", content)
|
||||
cfg := config.New()
|
||||
|
||||
err := cli.LoadConfiguration(cfg, tmpfile)
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLoadConfigurationInjectsHTTPTimeoutDefaults(t *testing.T) {
|
||||
Convey("load config sets HTTP read/write timeout defaults when not explicitly configured", t, func() {
|
||||
content := `{
|
||||
|
||||
Reference in New Issue
Block a user