Skip to content

Commit 98f8886

Browse files
committed
add RPC-based AUTHN and AUTHZ support
this change adds both a new authenticator as well as an authorizer implementation using RPC communication. the advantage over "External Authentication" is that those new plugins are only spawned once upon server start instead of with each auth request. this reduces resource problems in high request scenarios where the process IDs the system has available dwindle due to too many processes being created. the advantage over "Plugin Authentication" is that it works on all platforms. example plugins for each aspect (AUTHN/AUTHZ) have been implemented to demonstrate the feature as well as a simple test suite to verify the functionality. relates to #337
1 parent 78e779a commit 98f8886

41 files changed

Lines changed: 1384 additions & 90 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/go_test.yml

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,21 @@ jobs:
44
test:
55
strategy:
66
matrix:
7-
go-version: [1.23.x,1.24.x]
7+
go-version: [1.24.x, 1.25.x]
88
os: [ubuntu-latest]
99
runs-on: ${{ matrix.os }}
1010
steps:
11-
- name: Install Go
12-
uses: actions/setup-go@v5
13-
with:
14-
go-version: ${{ matrix.go-version }}
15-
- name: Checkout code
16-
uses: actions/checkout@v4
17-
- name: Test
18-
run: |
19-
cd auth_server
20-
go test ./...
21-
- name: Build
22-
run: |
23-
cd auth_server
24-
make
11+
- name: Install Go
12+
uses: actions/setup-go@v5
13+
with:
14+
go-version: ${{ matrix.go-version }}
15+
- name: Checkout code
16+
uses: actions/checkout@v4
17+
- name: Test
18+
run: |
19+
cd auth_server
20+
go test ./...
21+
- name: Build
22+
run: |
23+
cd auth_server
24+
make

auth_server/authn/ldap_auth.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ func NewLDAPAuth(c *LDAPAuthConfig) (*LDAPAuth, error) {
6161
}, nil
6262
}
6363

64-
//How to authenticate user, please refer to https://github.com/go-ldap/ldap/blob/master/example_test.go#L166
64+
// How to authenticate user, please refer to https://github.com/go-ldap/ldap/blob/master/example_test.go#L166
6565
func (la *LDAPAuth) Authenticate(account string, password api.PasswordString) (bool, api.Labels, error) {
6666
if account == "" || password == "" {
6767
return false, nil, api.NoMatch
@@ -160,10 +160,10 @@ func (la *LDAPAuth) bindInitialAsUser(l *ldap.Conn, account string, password api
160160
return nil
161161
}
162162

163-
//To prevent LDAP injection, some characters must be escaped for searching
164-
//e.g. char '\' will be replaced by hex '\5c'
165-
//Filter meta chars are choosen based on filter complier code
166-
//https://github.com/go-ldap/ldap/blob/master/filter.go#L159
163+
// To prevent LDAP injection, some characters must be escaped for searching
164+
// e.g. char '\' will be replaced by hex '\5c'
165+
// Filter meta chars are choosen based on filter complier code
166+
// https://github.com/go-ldap/ldap/blob/master/filter.go#L159
167167
func (la *LDAPAuth) escapeAccountInput(account string) string {
168168
r := strings.NewReplacer(
169169
`\`, `\5c`,
@@ -229,8 +229,8 @@ func (la *LDAPAuth) getFilter(account string) string {
229229
return filter
230230
}
231231

232-
//ldap search and return required attributes' value from searched entries
233-
//default return entry's DN value if you leave attrs array empty
232+
// ldap search and return required attributes' value from searched entries
233+
// default return entry's DN value if you leave attrs array empty
234234
func (la *LDAPAuth) ldapSearch(l *ldap.Conn, baseDN *string, filter *string, attrs *[]string) (string, map[string][]string, error) {
235235
if l == nil {
236236
return "", nil, fmt.Errorf("No ldap connection!")

auth_server/authn/mongo_auth.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,7 @@ func (mauth *MongoAuth) authenticate(account string, password api.PasswordString
102102
var dbUserRecord authUserEntry
103103
collection := mauth.session.Database(mauth.config.MongoConfig.DialInfo.Database).Collection(mauth.config.Collection)
104104

105-
106-
filter := bson.D{{"username", account}}
105+
filter := bson.D{{"username", account}}
107106
err := collection.FindOne(context.TODO(), filter).Decode(&dbUserRecord)
108107

109108
// If we connect and get no results we return a NoMatch so auth can fall-through

auth_server/authn/rpc_auth.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
Copyright 2019 Cesanta Software Ltd.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
https://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package authn
18+
19+
import (
20+
"fmt"
21+
"os/exec"
22+
23+
"github.com/cesanta/glog"
24+
rpc "github.com/hashicorp/go-plugin"
25+
26+
"github.com/cesanta/docker_auth/auth_server/api"
27+
shared "github.com/cesanta/docker_auth/auth_server/plugin"
28+
plugin "github.com/cesanta/docker_auth/auth_server/plugin/authn"
29+
)
30+
31+
type RPCAuthnConfig struct {
32+
Command string `yaml:"command"`
33+
Args []string `yaml:"args"`
34+
}
35+
36+
func (c *RPCAuthnConfig) Validate() error {
37+
if c.Command == "" {
38+
return fmt.Errorf("command is not set")
39+
}
40+
41+
if _, err := exec.LookPath(c.Command); err != nil {
42+
return fmt.Errorf("no such command: %s: %w", c.Command, err)
43+
}
44+
45+
return nil
46+
}
47+
48+
type RPCAuthn struct {
49+
client *rpc.Client
50+
impl plugin.Authenticator
51+
}
52+
53+
func (c *RPCAuthn) Authenticate(username string, password api.PasswordString) (bool, api.Labels, error) {
54+
req := &plugin.AuthenticateRequest{
55+
Username: username,
56+
Password: string(password),
57+
}
58+
resp, err := c.impl.Authenticate(req)
59+
switch {
60+
case err == nil:
61+
return true, api.Labels(resp), nil
62+
case shared.IsError(err, shared.ErrUnauthorized):
63+
return false, nil, nil
64+
case shared.IsError(err, shared.ErrUnacceptable):
65+
return false, nil, api.NoMatch
66+
default:
67+
return false, nil, err
68+
}
69+
}
70+
71+
func (c *RPCAuthn) Stop() {
72+
if c.client != nil {
73+
c.client.Kill()
74+
}
75+
}
76+
77+
func (c *RPCAuthn) Name() string {
78+
return "rpc"
79+
}
80+
81+
func NewRPCAuthn(cfg *RPCAuthnConfig) (*RPCAuthn, error) {
82+
glog.Infof("RPC authenticator: %s", cfg)
83+
84+
conn := &rpc.ClientConfig{
85+
HandshakeConfig: plugin.Handshake,
86+
Plugins: plugin.PluginMap,
87+
Cmd: exec.Command(cfg.Command, cfg.Args...),
88+
}
89+
client := rpc.NewClient(conn)
90+
91+
rpcClient, err := client.Client()
92+
if err != nil {
93+
client.Kill()
94+
return nil, err
95+
}
96+
97+
raw, err := rpcClient.Dispense(plugin.PluginNetRPC)
98+
if err != nil {
99+
client.Kill()
100+
return nil, err
101+
}
102+
103+
impl, ok := raw.(plugin.Authenticator)
104+
if !ok {
105+
client.Kill()
106+
return nil, fmt.Errorf("no authenticator plugin provided: %T", impl)
107+
}
108+
109+
result := &RPCAuthn{
110+
client: client,
111+
impl: impl,
112+
}
113+
114+
return result, nil
115+
}

auth_server/authn/tokendb_gcs.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ func NewGCSTokenDB(options *GCSStoreConfig) (TokenDB, error) {
5151
}
5252

5353
type gcsTokenDB struct {
54-
gcs *storage.Client
55-
bucket string
54+
gcs *storage.Client
55+
bucket string
5656
tokenHashCost int
5757
}
5858

auth_server/authn/tokendb_level.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ const (
3737
var ExpiredToken = errors.New("expired token")
3838

3939
type LevelDBStoreConfig struct {
40-
Path string `yaml:"path,omitempty"`
41-
TokenHashCost int `yaml:"token_hash_cost,omitempty"`
40+
Path string `yaml:"path,omitempty"`
41+
TokenHashCost int `yaml:"token_hash_cost,omitempty"`
4242
}
4343

4444
// TokenDB stores tokens using LevelDB

auth_server/authn/tokendb_redis.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ type RedisClient interface {
4242
}
4343

4444
// NewRedisTokenDB returns a new TokenDB structure which uses Redis as the storage backend.
45-
//
4645
func NewRedisTokenDB(options *RedisStoreConfig) (TokenDB, error) {
4746
var client RedisClient
4847
if options.ClusterOptions != nil {
@@ -58,11 +57,11 @@ func NewRedisTokenDB(options *RedisStoreConfig) (TokenDB, error) {
5857
tokenHashCost = bcrypt.DefaultCost
5958
}
6059

61-
return &redisTokenDB{client,tokenHashCost}, nil
60+
return &redisTokenDB{client, tokenHashCost}, nil
6261
}
6362

6463
type redisTokenDB struct {
65-
client RedisClient
64+
client RedisClient
6665
tokenHashCost int
6766
}
6867

auth_server/authn/xorm_sqlite_authn.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
//+build sqlite
1+
//go:build sqlite
2+
// +build sqlite
23

34
/*
45
Copyright 2020 Cesanta Software Ltd.

auth_server/authz/acl_xorm_sqlite.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
//+build sqlite
1+
//go:build sqlite
2+
// +build sqlite
23

34
/*
45
Copyright 2020 Cesanta Software Ltd.

auth_server/authz/rpc_auth.go

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/*
2+
Copyright 2019 Cesanta Software Ltd.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
https://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package authz
18+
19+
import (
20+
"fmt"
21+
"os/exec"
22+
23+
"github.com/cesanta/glog"
24+
rpc "github.com/hashicorp/go-plugin"
25+
26+
"github.com/cesanta/docker_auth/auth_server/api"
27+
shared "github.com/cesanta/docker_auth/auth_server/plugin"
28+
plugin "github.com/cesanta/docker_auth/auth_server/plugin/authz"
29+
)
30+
31+
type RPCAuthzConfig struct {
32+
Command string `yaml:"command"`
33+
Args []string `yaml:"args"`
34+
}
35+
36+
func (c *RPCAuthzConfig) Validate() error {
37+
if c.Command == "" {
38+
return fmt.Errorf("command is not set")
39+
}
40+
41+
if _, err := exec.LookPath(c.Command); err != nil {
42+
return fmt.Errorf("no such command: %s: %w", c.Command, err)
43+
}
44+
45+
return nil
46+
}
47+
48+
type RPCAuthz struct {
49+
client *rpc.Client
50+
impl plugin.Authorizer
51+
}
52+
53+
func (c *RPCAuthz) Authorize(ai *api.AuthRequestInfo) ([]string, error) {
54+
req := &plugin.AuthorizeRequest{
55+
Account: ai.Account,
56+
Type: ai.Type,
57+
Name: ai.Name,
58+
Service: ai.Service,
59+
IP: ai.IP,
60+
Actions: ai.Actions,
61+
Labels: ai.Labels,
62+
}
63+
resp, err := c.impl.Authorize(req)
64+
switch {
65+
case err == nil:
66+
return resp, nil
67+
case shared.IsError(err, shared.ErrForbidden):
68+
return []string{}, nil
69+
case shared.IsError(err, shared.ErrUnacceptable):
70+
return nil, api.NoMatch
71+
default:
72+
return nil, err
73+
}
74+
}
75+
76+
func (c *RPCAuthz) Stop() {
77+
if c.client != nil {
78+
c.client.Kill()
79+
}
80+
}
81+
82+
func (c *RPCAuthz) Name() string {
83+
return "rpc"
84+
}
85+
86+
func NewRPCAuthz(cfg *RPCAuthzConfig) (*RPCAuthz, error) {
87+
glog.Infof("RPC authorizer: %s", cfg)
88+
89+
conn := &rpc.ClientConfig{
90+
HandshakeConfig: plugin.Handshake,
91+
Plugins: plugin.PluginMap,
92+
Cmd: exec.Command(cfg.Command, cfg.Args...),
93+
}
94+
client := rpc.NewClient(conn)
95+
96+
rpcClient, err := client.Client()
97+
if err != nil {
98+
client.Kill()
99+
return nil, err
100+
}
101+
102+
raw, err := rpcClient.Dispense(plugin.PluginNetRPC)
103+
if err != nil {
104+
client.Kill()
105+
return nil, err
106+
}
107+
108+
impl, ok := raw.(plugin.Authorizer)
109+
if !ok {
110+
client.Kill()
111+
return nil, fmt.Errorf("no authorizer plugin provided: %T", impl)
112+
}
113+
114+
result := &RPCAuthz{
115+
client: client,
116+
impl: impl,
117+
}
118+
119+
return result, nil
120+
}

0 commit comments

Comments
 (0)