Skip to content

Commit

Permalink
RBAC Foundations (#12)
Browse files Browse the repository at this point in the history
Enable the encode of group and role information into the oauth2 access
token.
  • Loading branch information
spjmurray authored Mar 14, 2024
1 parent 62b7ec9 commit aca4a77
Show file tree
Hide file tree
Showing 9 changed files with 123 additions and 24 deletions.
33 changes: 25 additions & 8 deletions charts/identity/crds/identity.unikorn-cloud.org_organizations.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -61,25 +61,42 @@ spec:
are specified then it is assumed all users have access to everything.
items:
properties:
displayName:
description: DisplayName is the name to display the group as
in UIs and other UX interfaces. This should again be unique
within the organization to avoid ambiguity, but may be changed.
type: string
id:
description: ID is the a unique, and immutable identifier for
the group, the intent being that resources will belong to
a group irrespective of display name changes.
type: string
providerName:
name:
description: Name is the name to display the group as in UIs
and other UX interfaces. This should again be unique within
the organization to avoid ambiguity, but may be changed.
type: string
providerGroupName:
description: ProviderName is the name of the group as returned
by the provider. For example a query of https://cloudidentity.googleapis.com/v1/groups/
will return something like groups/01664s551ax43ok.
type: string
roles:
description: Roles are a list of roles users of the group inherit.
items:
description: Role defines the role a user has within the scope
of a group.
enum:
- superAdmin
- admin
- user
- reader
type: string
type: array
users:
description: Users are a list of user names that are members
of the group.
items:
type: string
type: array
required:
- displayName
- id
- providerName
- name
type: object
type: array
owner:
Expand Down
19 changes: 19 additions & 0 deletions charts/identity/templates/organization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,23 @@ spec:
customerId: {{ $google.customerId }}
{{- end }}
{{- end }}
{{- with $groups := $org.groups }}
groups:
{{- range $group := $groups }}
- id: {{ $group.id }}
name: {{ $group.name }}
{{- with $roles := $group.roles }}
roles:
{{- range $role := $roles }}
- {{ $role }}
{{- end }}
{{- end }}
{{- with $users := $group.users }}
users:
{{- range $user := $users }}
- {{ $user }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
8 changes: 7 additions & 1 deletion charts/identity/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,15 @@ host: identity.acme.org
# - # A unique and immutable ID.
# id: 5f7033e8-17fc-41a1-a8f6-f3085115b611
# # The name to be displayed by UIs.
# displayName: My Group
# name: My Group
# # The unique name the provider refers to the group.
# providerName: groups/38ru24810eck10
# # Explicit list of users
# users:
# - foo@gmail.com
# # Roles for the group e.g. superAdmin, admin, user, reader.
# roles:
# - user

ingress:
# Sets the ingress class to use.
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ require (
github.com/go-jose/go-jose/v3 v3.0.1
github.com/google/uuid v1.6.0
github.com/spf13/pflag v1.0.5
github.com/unikorn-cloud/core v0.1.8
github.com/unikorn-cloud/core v0.1.10
go.opentelemetry.io/otel v1.24.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0
go.opentelemetry.io/otel/sdk v1.22.0
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,12 @@ github.com/unikorn-cloud/core v0.1.7 h1:I2Xu9gYlRnkG0TjY+qbTIWY+kHCv76mCOVFkLL1d
github.com/unikorn-cloud/core v0.1.7/go.mod h1:G45rJ0e5LOdoFcD9C00wSuhe/AMeBC+tczmQSsS+0/Q=
github.com/unikorn-cloud/core v0.1.8 h1:JJAfUgCP2hAAAMcfWjE0hmyDOmlg9fxNlWSyJs7gT3k=
github.com/unikorn-cloud/core v0.1.8/go.mod h1:G45rJ0e5LOdoFcD9C00wSuhe/AMeBC+tczmQSsS+0/Q=
github.com/unikorn-cloud/core v0.1.10-0.20240314110053-42e6a0300b53 h1:vQYiOBQoUBpuu8XnHskUe8ypnK4M9TnwlrvoO84f4F0=
github.com/unikorn-cloud/core v0.1.10-0.20240314110053-42e6a0300b53/go.mod h1:G45rJ0e5LOdoFcD9C00wSuhe/AMeBC+tczmQSsS+0/Q=
github.com/unikorn-cloud/core v0.1.10-0.20240314111057-273eb5d18310 h1:xKFgb2hVz7sLzFk+KI7POiaaniibvkuiclR3pMlUOSs=
github.com/unikorn-cloud/core v0.1.10-0.20240314111057-273eb5d18310/go.mod h1:G45rJ0e5LOdoFcD9C00wSuhe/AMeBC+tczmQSsS+0/Q=
github.com/unikorn-cloud/core v0.1.10 h1:8D5+CSbBi0ziutAoaWnQq/t1mtfV0IQ1uD6Dg7HfAqY=
github.com/unikorn-cloud/core v0.1.10/go.mod h1:G45rJ0e5LOdoFcD9C00wSuhe/AMeBC+tczmQSsS+0/Q=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
Expand Down
12 changes: 9 additions & 3 deletions pkg/apis/unikorn/v1alpha1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ limitations under the License.
package v1alpha1

import (
"github.com/unikorn-cloud/core/pkg/authorization/oauth2/claims"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

Expand Down Expand Up @@ -174,14 +176,18 @@ type OrganizationGroup struct {
// being that resources will belong to a group irrespective of display name
// changes.
ID string `json:"id"`
// DisplayName is the name to display the group as in UIs and other UX
// Name is the name to display the group as in UIs and other UX
// interfaces. This should again be unique within the organization to
// avoid ambiguity, but may be changed.
DisplayName string `json:"displayName"`
Name string `json:"name"`
// ProviderName is the name of the group as returned by the provider.
// For example a query of https://cloudidentity.googleapis.com/v1/groups/
// will return something like groups/01664s551ax43ok.
ProviderName string `json:"providerName"`
ProviderGroupName *string `json:"providerGroupName,omitempty"`
// Users are a list of user names that are members of the group.
Users []string `json:"users,omitempty"`
// Roles are a list of roles users of the group inherit.
Roles []claims.Role `json:"roles,omitempty"`
}

// OrganizationStatus defines the status of the server.
Expand Down
20 changes: 19 additions & 1 deletion pkg/apis/unikorn/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 31 additions & 3 deletions pkg/oauth2/accesstoken.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,17 @@ import (
"errors"
"fmt"
"net/http"
"slices"
"time"

"github.com/go-jose/go-jose/v3/jwt"
"github.com/google/uuid"

"github.com/unikorn-cloud/core/pkg/authorization/oauth2/claims"
unikornv1 "github.com/unikorn-cloud/identity/pkg/apis/unikorn/v1alpha1"
"github.com/unikorn-cloud/identity/pkg/jose"

"sigs.k8s.io/controller-runtime/pkg/client"
)

var (
Expand All @@ -40,7 +44,28 @@ var (
)

// Issue issues a new JWT access token.
func Issue(i *jose.JWTIssuer, r *http.Request, code *Code, expiresAt time.Time) (string, error) {
func (a *Authenticator) Issue(i *jose.JWTIssuer, r *http.Request, code *Code, expiresAt time.Time) (string, error) {
var organization unikornv1.Organization

if err := a.client.Get(r.Context(), client.ObjectKey{Namespace: a.namespace, Name: code.Organization}, &organization); err != nil {
return "", err
}

//nolint:prealloc
var groups []claims.Group

for _, group := range organization.Spec.Groups {
// TODO: we should also check if the IdP has mapped the user to a group here.
if !slices.Contains(group.Users, code.Subject) {
continue
}

groups = append(groups, claims.Group{
ID: group.ID,
Roles: group.Roles,
})
}

now := time.Now()

nowRFC7519 := jwt.NumericDate(now.Unix())
Expand All @@ -49,7 +74,7 @@ func Issue(i *jose.JWTIssuer, r *http.Request, code *Code, expiresAt time.Time)
claims := &claims.Claims{
Claims: jwt.Claims{
ID: uuid.New().String(),
Subject: code.Email,
Subject: code.Subject,
Audience: jwt.Audience{
code.ClientID,
},
Expand All @@ -58,7 +83,10 @@ func Issue(i *jose.JWTIssuer, r *http.Request, code *Code, expiresAt time.Time)
NotBefore: &nowRFC7519,
Expiry: &expiresAtRFC7519,
},
Organization: code.Organization,
Unikorn: &claims.UnikornClaims{
Organization: code.Organization,
Groups: groups,
},
}

token, err := i.EncodeJWT(claims)
Expand Down
13 changes: 6 additions & 7 deletions pkg/oauth2/federated.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,8 @@ type Code struct {
ClientScope scope.Scope `json:"csc,omitempty"`
// ClientNonce is injected into a OIDC id_token.
ClientNonce string `json:"cno,omitempty"`
// TODO: we would be a lot more flexible by passing all the claims over...
// Email is exactly that.
Email string `json:"email"`
// Subject is the canonical subject name (not an alias).
Subject string `json:"sub"`
// Organization is the user's organization name.
Organization string `json:"org"`
}
Expand Down Expand Up @@ -634,15 +633,15 @@ func (a *Authenticator) OIDCCallback(w http.ResponseWriter, r *http.Request) {

// TODO: map from IdP groups to ours and add into the code for later encoding
// into the access token.

// NOTE: the email
oauth2Code := &Code{
ClientID: state.ClientID,
ClientRedirectURI: state.ClientRedirectURI,
ClientCodeChallenge: state.ClientCodeChallenge,
ClientScope: state.ClientScope,
ClientNonce: state.ClientNonce,
Organization: state.Organization,
Email: claims.Email,
Subject: claims.Email,
}

code, err := a.issuer.EncodeJWEToken(oauth2Code)
Expand Down Expand Up @@ -777,13 +776,13 @@ func (a *Authenticator) Token(w http.ResponseWriter, r *http.Request) (*generate
expiry := time.Now().Add(24 * time.Hour)

// TODO: add some scopes, these hould probably be derived from the organization.
accessToken, err := Issue(a.issuer, r, code, expiry)
accessToken, err := a.Issue(a.issuer, r, code, expiry)
if err != nil {
return nil, err
}

// Handle OIDC.
idToken, err := a.oidcIDToken(r, code.ClientScope, expiry, oidcHash(accessToken), r.Form.Get("client_id"), code.Email)
idToken, err := a.oidcIDToken(r, code.ClientScope, expiry, oidcHash(accessToken), r.Form.Get("client_id"), code.Subject)
if err != nil {
return nil, err
}
Expand Down

0 comments on commit aca4a77

Please sign in to comment.