-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
WIP: Add optional access control to API
Optionally enable Attribute Based Access Control (ABAC) in the API. When enabled, the API will look for a custom claim in the access token and extract the attributes relevant to Enduro. If a custom scope needs to be requested to get that claim, it has to be configured in the dashboard. - Add OIDC ABAC configuration. Allows to enable or disable access control entirely and set a claim to get the attributes. That claim could be nested and include values unrelated to Enduro. - Extend claims and token verifier to parse ABAC attributes from the access token based on configuration. - Allow custom scopes in dashboard and configure it to work with the Keycloak instance for the dev. env. by default. - Add scopes, forbidden error and security to a few endpoints in Goa's API design. - Check scopes against attributes in API requests. As a first access control check, the scopes configured on the API design need to be included in the user attributes. - This check allows attributes with wildcards, so: - `package:read` will only provide access to that action(s). - `package:*` will provide access to all package related actions. - `*` will provide full access to all actions. - This attributes/scopes need to be defined and extended across the API and they don't have to be related to a single action/endpoint. - Add claims with the extracted attributes to the context on each request. After that initial check is passed, the claims are included in the context to make them available for other possible access control checks on the endpoints implementation.
- Loading branch information
Showing
42 changed files
with
2,046 additions
and
84 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
VITE_OIDC_AUTHORITY=\$VITE_OIDC_AUTHORITY | ||
VITE_OIDC_CLIENT_ID=\$VITE_OIDC_CLIENT_ID | ||
VITE_OIDC_REDIRECT_URI=\$VITE_OIDC_REDIRECT_URI | ||
VITE_OIDC_EXTRA_SCOPES=\$VITE_OIDC_EXTRA_SCOPES |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
VITE_OIDC_AUTHORITY=http://keycloak:7470/realms/enduro | ||
VITE_OIDC_CLIENT_ID=enduro | ||
VITE_OIDC_REDIRECT_URI=http://localhost:8080/user/signin-callback | ||
VITE_OIDC_EXTRA_SCOPES=enduro |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,14 @@ | ||
import { UserManager, WebStorageStateStore } from "oidc-client-ts"; | ||
|
||
let scope = "openid email profile"; | ||
if (import.meta.env.VITE_OIDC_EXTRA_SCOPES != undefined) { | ||
scope += " " + import.meta.env.VITE_OIDC_EXTRA_SCOPES; | ||
} | ||
|
||
export default new UserManager({ | ||
authority: import.meta.env.VITE_OIDC_AUTHORITY, | ||
client_id: import.meta.env.VITE_OIDC_CLIENT_ID, | ||
redirect_uri: import.meta.env.VITE_OIDC_REDIRECT_URI, | ||
scope: "openid email profile", | ||
scope: scope, | ||
userStore: new WebStorageStateStore({ store: window.localStorage }), | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
package auth | ||
|
||
import ( | ||
"context" | ||
"slices" | ||
"strings" | ||
) | ||
|
||
type Claims struct { | ||
Email string `json:"email,omitempty"` | ||
EmailVerified bool `json:"email_verified,omitempty"` | ||
Attributes []string `json:"-"` | ||
} | ||
|
||
// CheckAttributes verifies all required attributes are present in the claim | ||
// attributes. It always verifies if the claim is nil (authentication disabled) | ||
// or the attributes are nil (access control disabled). Attributes are verified | ||
// by exact match or by having an ancestor with wildcard. For example, a claim | ||
// with "*" or "package:*" as one of it's attributes will verify all package | ||
// actions, like "package:list", "package:read", etc. | ||
func (c *Claims) CheckAttributes(required []string) bool { | ||
// Authentication disabled, access control disabled or all wildcard in claims. | ||
if c == (*Claims)(nil) || c.Attributes == nil || slices.Contains(c.Attributes, "*") { | ||
return true | ||
} | ||
|
||
// Check for all required attributes considering wildcards. | ||
for _, attr := range required { | ||
for { | ||
if slices.Contains(c.Attributes, attr) { | ||
break | ||
} | ||
attr, _ = strings.CutSuffix(attr, ":*") | ||
lastColonIndex := strings.LastIndex(attr, ":") | ||
if lastColonIndex == -1 { | ||
return false | ||
} | ||
|
||
attr = attr[:lastColonIndex] + ":*" | ||
} | ||
} | ||
|
||
return true | ||
} | ||
|
||
type contextUserClaimsType struct{} | ||
|
||
var contextUserClaimsKey = &contextUserClaimsType{} | ||
|
||
// WithUserClaims puts the user claims into the current context. | ||
func WithUserClaims(ctx context.Context, claims *Claims) context.Context { | ||
return context.WithValue(ctx, contextUserClaimsKey, claims) | ||
} | ||
|
||
// UserClaimsFromContext returns the user claims from the context. | ||
// An empty string is returned if it was not found. | ||
func UserClaimsFromContext(ctx context.Context) *Claims { | ||
v := ctx.Value(contextUserClaimsKey) | ||
if v == nil { | ||
return nil | ||
} | ||
c, ok := v.(*Claims) | ||
if !ok { | ||
return nil | ||
} | ||
return c | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
package auth_test | ||
|
||
import ( | ||
"context" | ||
"testing" | ||
|
||
"gotest.tools/v3/assert" | ||
|
||
"github.com/artefactual-sdps/enduro/internal/api/auth" | ||
) | ||
|
||
func TestUserClaimsFromContext(t *testing.T) { | ||
t.Parallel() | ||
|
||
t.Run("Returns claims when found", func(t *testing.T) { | ||
t.Parallel() | ||
|
||
claims := auth.Claims{ | ||
Email: "[email protected]", | ||
EmailVerified: true, | ||
Attributes: []string{"*"}, | ||
} | ||
|
||
ctx := context.Background() | ||
ctx = auth.WithUserClaims(ctx, &claims) | ||
assert.Equal(t, auth.UserClaimsFromContext(ctx), &claims) | ||
}) | ||
|
||
t.Run("Returns nil when not found", func(t *testing.T) { | ||
t.Parallel() | ||
|
||
ctx := context.Background() | ||
assert.Equal(t, auth.UserClaimsFromContext(ctx), (*auth.Claims)(nil)) | ||
}) | ||
} | ||
|
||
func TestCheckAttributes(t *testing.T) { | ||
t.Parallel() | ||
|
||
type test struct { | ||
name string | ||
claims *auth.Claims | ||
attributes []string | ||
want bool | ||
} | ||
for _, tt := range []test{ | ||
{ | ||
name: "Checks without required attributes", | ||
claims: &auth.Claims{ | ||
Attributes: []string{}, | ||
}, | ||
attributes: []string{}, | ||
want: true, | ||
}, | ||
{ | ||
name: "Checks a single attribute exists", | ||
claims: &auth.Claims{ | ||
Attributes: []string{"package:list"}, | ||
}, | ||
attributes: []string{"package:list"}, | ||
want: true, | ||
}, | ||
{ | ||
name: "Checks multiple attributes exist", | ||
claims: &auth.Claims{ | ||
Attributes: []string{"package:list", "package:read"}, | ||
}, | ||
attributes: []string{"package:list", "package:read"}, | ||
want: true, | ||
}, | ||
{ | ||
name: "Checks attribute is missing", | ||
claims: &auth.Claims{ | ||
Attributes: []string{}, | ||
}, | ||
attributes: []string{"package:download"}, | ||
want: false, | ||
}, | ||
{ | ||
name: "Checks attributes on nil claim (auth disabled)", | ||
claims: (*auth.Claims)(nil), | ||
attributes: []string{"package:list"}, | ||
want: true, | ||
}, | ||
{ | ||
name: "Checks attributes on nil attributes (ABAC disabled)", | ||
claims: &auth.Claims{}, | ||
attributes: []string{"package:list"}, | ||
want: true, | ||
}, | ||
{ | ||
name: "Checks attributes with wildcards", | ||
claims: &auth.Claims{ | ||
Attributes: []string{"package:*", "storage:*"}, | ||
}, | ||
attributes: []string{"package:list:something", "storage:download"}, | ||
want: true, | ||
}, | ||
{ | ||
name: "Checks attributes with all wildcard", | ||
claims: &auth.Claims{ | ||
Attributes: []string{"*"}, | ||
}, | ||
attributes: []string{"package:list", "storage:download"}, | ||
want: true, | ||
}, | ||
{ | ||
name: "Checks missing attributes with wildcard", | ||
claims: &auth.Claims{ | ||
Attributes: []string{"package:*"}, | ||
}, | ||
attributes: []string{"package:list", "storage:download"}, | ||
want: false, | ||
}, | ||
} { | ||
t.Run(tt.name, func(t *testing.T) { | ||
t.Parallel() | ||
|
||
assert.Equal(t, tt.claims.CheckAttributes(tt.attributes), tt.want) | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.