Skip to content

Commit 6a933de

Browse files
authored
feat: authorization backend for API tokens (#474)
Signed-off-by: Miguel Martinez Trivino <[email protected]>
1 parent 821eb7d commit 6a933de

File tree

24 files changed

+937
-46
lines changed

24 files changed

+937
-46
lines changed

app/controlplane/cmd/main.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ func init() {
5757
flag.StringVar(&flagconf, "conf", "../configs", "config path, eg: -conf config.yaml")
5858
}
5959

60-
func newApp(logger log.Logger, gs *grpc.Server, hs *http.Server, ms *server.HTTPMetricsServer, expirer *biz.WorkflowRunExpirerUseCase, plugins sdk.AvailablePlugins) *app {
60+
func newApp(logger log.Logger, gs *grpc.Server, hs *http.Server, ms *server.HTTPMetricsServer, expirer *biz.WorkflowRunExpirerUseCase, plugins sdk.AvailablePlugins, tokenSync *biz.APITokenSyncerUseCase) *app {
6161
return &app{
6262
kratos.New(
6363
kratos.ID(id),
@@ -66,7 +66,7 @@ func newApp(logger log.Logger, gs *grpc.Server, hs *http.Server, ms *server.HTTP
6666
kratos.Metadata(map[string]string{}),
6767
kratos.Logger(logger),
6868
kratos.Server(gs, hs, ms),
69-
), expirer, plugins}
69+
), expirer, plugins, tokenSync}
7070
}
7171

7272
func main() {
@@ -129,6 +129,15 @@ func main() {
129129
// TODO: Make it configurable from the application config
130130
app.runsExpirer.Run(ctx, &biz.WorkflowRunExpirerOpts{CheckInterval: 1 * time.Minute, ExpirationWindow: 1 * time.Hour})
131131

132+
// Since policies management is not enabled yet but instead is based on a hardcoded list of permissions
133+
// We'll perform a reconciliation of the policies with the tokens stored in the database on startup
134+
// This will allow us to add more policies in the future and keep backwards compatibility with existing tokens
135+
go func() {
136+
if err := app.tokenAuthSyncer.SyncPolicies(); err != nil {
137+
_ = logger.Log(log.LevelError, "msg", "syncing policies", "error", err)
138+
}
139+
}()
140+
132141
// start and wait for stop signal
133142
if err := app.Run(); err != nil {
134143
panic(err)
@@ -140,6 +149,7 @@ type app struct {
140149
// Periodic job that expires unfinished attestation processes older than a given threshold
141150
runsExpirer *biz.WorkflowRunExpirerUseCase
142151
availablePlugins sdk.AvailablePlugins
152+
tokenAuthSyncer *biz.APITokenSyncerUseCase
143153
}
144154

145155
func filterSensitiveArgs(_ log.Level, keyvals ...interface{}) bool {

app/controlplane/cmd/wire.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
package main
2222

2323
import (
24+
"github.com/chainloop-dev/chainloop/app/controlplane/internal/authz"
2425
"github.com/chainloop-dev/chainloop/app/controlplane/internal/biz"
2526
"github.com/chainloop-dev/chainloop/app/controlplane/internal/conf"
2627
"github.com/chainloop-dev/chainloop/app/controlplane/internal/data"
@@ -47,7 +48,9 @@ func wireApp(*conf.Bootstrap, credentials.ReaderWriter, log.Logger, sdk.Availabl
4748
serviceOpts,
4849
wire.Value([]biz.CASClientOpts{}),
4950
wire.FieldsOf(new(*conf.Bootstrap), "Server", "Auth", "Data", "CasServer", "ReferrerSharedIndex"),
51+
wire.FieldsOf(new(*conf.Data), "Database"),
5052
dispatcher.New,
53+
authz.NewDatabaseEnforcer,
5154
newApp,
5255
),
5356
)

app/controlplane/cmd/wire_gen.go

Lines changed: 11 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
//
2+
// Copyright 2024 The Chainloop Authors.
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+
// http://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+
// Authorization package
17+
package authz
18+
19+
import (
20+
"errors"
21+
"fmt"
22+
23+
_ "embed"
24+
25+
"github.com/casbin/casbin/v2"
26+
"github.com/casbin/casbin/v2/model"
27+
"github.com/casbin/casbin/v2/persist"
28+
fileadapter "github.com/casbin/casbin/v2/persist/file-adapter"
29+
entadapter "github.com/casbin/ent-adapter"
30+
"github.com/chainloop-dev/chainloop/app/controlplane/internal/conf"
31+
)
32+
33+
const (
34+
// Actions
35+
ActionRead = "read"
36+
ActionList = "list"
37+
ActionUpdate = "update"
38+
ActionDelete = "delete"
39+
40+
// Resources
41+
ResourceWorkflowContract = "workflow_contract"
42+
ResourceCASArtifact = "cas_artifact"
43+
ResourceReferrer = "referrer"
44+
)
45+
46+
// resource, action tuple
47+
type Policy struct {
48+
Resource string
49+
Action string
50+
}
51+
52+
var (
53+
PolicyWorkflowContractList = &Policy{ResourceWorkflowContract, ActionList}
54+
PolicyWorkflowContractRead = &Policy{ResourceWorkflowContract, ActionRead}
55+
PolicyWorkflowContractUpdate = &Policy{ResourceWorkflowContract, ActionUpdate}
56+
PolicyArtifactDownload = &Policy{ResourceCASArtifact, ActionRead}
57+
PolicyReferrerRead = &Policy{ResourceReferrer, ActionRead}
58+
)
59+
60+
type SubjectAPIToken struct {
61+
ID string
62+
}
63+
64+
func (t *SubjectAPIToken) String() string {
65+
return fmt.Sprintf("api-token:%s", t.ID)
66+
}
67+
68+
//go:embed model.conf
69+
var modelFile []byte
70+
71+
type Enforcer struct {
72+
*casbin.Enforcer
73+
}
74+
75+
func (e *Enforcer) AddPolicies(sub *SubjectAPIToken, policies ...*Policy) error {
76+
if len(policies) == 0 {
77+
return errors.New("no policies to add")
78+
}
79+
80+
if sub == nil {
81+
return errors.New("no subject provided")
82+
}
83+
84+
for _, p := range policies {
85+
casbinPolicy := []string{sub.String(), p.Resource, p.Action}
86+
// Add policies one by one to skip existing ones.
87+
// This is because the bulk method AddPoliciesEx does not work well with the ent adapter
88+
if _, err := e.AddPolicy(casbinPolicy); err != nil {
89+
return fmt.Errorf("failed to add policy: %w", err)
90+
}
91+
}
92+
93+
return nil
94+
}
95+
96+
// Remove all the policies for the given subject
97+
func (e *Enforcer) ClearPolicies(sub *SubjectAPIToken) error {
98+
if sub == nil {
99+
return errors.New("no subject provided")
100+
}
101+
102+
// Get all the policies for the subject
103+
policies := e.GetFilteredPolicy(0, sub.String())
104+
105+
if _, err := e.Enforcer.RemovePolicies(policies); err != nil {
106+
return fmt.Errorf("failed to remove policies: %w", err)
107+
}
108+
109+
return nil
110+
}
111+
112+
// NewDatabaseEnforcer creates a new casbin authorization enforcer
113+
// based on a database backend as policies storage backend
114+
func NewDatabaseEnforcer(c *conf.Data_Database) (*Enforcer, error) {
115+
// policy storage in database
116+
a, err := entadapter.NewAdapter(c.Driver, c.Source)
117+
if err != nil {
118+
return nil, fmt.Errorf("failed to create adapter: %w", err)
119+
}
120+
121+
e, err := newEnforcer(a)
122+
if err != nil {
123+
return nil, fmt.Errorf("failed to create enforcer: %w", err)
124+
}
125+
126+
return e, nil
127+
}
128+
129+
// NewFileAdapter creates a new casbin authorization enforcer
130+
// based on a CSV file as policies storage backend
131+
func NewFiletypeEnforcer(path string) (*Enforcer, error) {
132+
// policy storage in filesystem
133+
a := fileadapter.NewAdapter(path)
134+
e, err := newEnforcer(a)
135+
if err != nil {
136+
return nil, fmt.Errorf("failed to create enforcer: %w", err)
137+
}
138+
139+
return e, nil
140+
}
141+
142+
// NewEnforcer creates a new casbin authorization enforcer for the policies stored
143+
// in the database and the model defined in model.conf
144+
func newEnforcer(a persist.Adapter) (*Enforcer, error) {
145+
// load model defined in model.conf
146+
m, err := model.NewModelFromString(string(modelFile))
147+
if err != nil {
148+
return nil, fmt.Errorf("failed to create model: %w", err)
149+
}
150+
151+
// create enforcer for authorization
152+
enforcer, err := casbin.NewEnforcer(m, a)
153+
if err != nil {
154+
return nil, fmt.Errorf("failed to create enforcer: %w", err)
155+
}
156+
157+
return &Enforcer{enforcer}, nil
158+
}

0 commit comments

Comments
 (0)