Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ spec:
namespace: krateo-system
filter: .groups.value | map(.displayName)
```
To obtain groups through the rest action, add `Directory.Read.All` in the `additionalScopes`.
To obtain groups through the rest action, add `Directory.Read.All` in the `additionalScopes`. There is a more complex example for the Microsoft Entra API that also uses pagination in the testdata folder (requires Authn >= 0.22.0).

On AuthN:
- To obtain the user avatar/profile image include `User.Read` in the `additionalScopes` field of the OIDCConfiguration custom resource;
Expand Down
42 changes: 42 additions & 0 deletions internal/helpers/restaction/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,48 @@ type Response struct {
}

func Resolve(ctx context.Context, rc *rest.Config, restaction *core.ObjectRef, email string, bearerToken string) (map[string]interface{}, error) {
jsonToken, _ := json.Marshal(map[string]string{
"token": bearerToken,
})
// Call the RESTAction from snowplow
url := fmt.Sprintf(ctx.Value(RestActionContextKey("snowplowURL")).(string)+
"/call?apiVersion=templates.krateo.io%%2Fv1&resource=restactions&name=%s&namespace=%s&extras=%s",
restaction.Name,
restaction.Namespace,
jsonToken)
request, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create http request for restaction call to snowplow: %w", err)
}

jwt, ok := xcontext.AccessToken(ctx)
if !ok {
return nil, fmt.Errorf("failed to retrieve jwt token for authn")
}
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", jwt))

resp, err := http.DefaultClient.Do(request)
if err != nil {
return nil, fmt.Errorf("failed to send http request for restaction call to snowplow: %w", err)
}
responseRaw, _ := io.ReadAll(resp.Body)
defer resp.Body.Close()

if resp.StatusCode != 200 {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("snowplow endpoint returned a non-200 status code: %s", string(body))
}

var responseStruct Response
err = json.Unmarshal(responseRaw, &responseStruct)
if err != nil {
return nil, fmt.Errorf("error parsing userinfo payload JSON: %v\npayload: %s", err, string(responseRaw))
}

return responseStruct.Status, nil
}

func LegacyResolve(ctx context.Context, rc *rest.Config, restaction *core.ObjectRef, email string, bearerToken string) (map[string]interface{}, error) {
restactionCopy, err := copyRestActionWithEndpoints(ctx, rc, restaction, email, bearerToken)
if err != nil {
return nil, fmt.Errorf("could not resolve restaction: %v", err)
Expand Down
14 changes: 10 additions & 4 deletions internal/routes/auth/oauth/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,16 @@ func (r *loginRoute) Handler() http.HandlerFunc {
return
}
additionalFieldstoReplace, err := restaction.Resolve(r.ctx, r.rc, restactionRef, uuid.New().String(), tok.AccessToken)
if err != nil {
log.Err(err).Str("name", name).Msg("unable to resolve restaction")
encode.InternalError(wri, err)
return
value, ok := additionalFieldstoReplace["name"]
_, okk := additionalFieldstoReplace["name"].(string)
if err != nil || !ok || !okk || value == nil {
log.Err(err).Str("name", name).Msg("unable to resolve restaction, retrying with legacy resolve (copy)")
additionalFieldstoReplace, err = restaction.LegacyResolve(r.ctx, r.rc, restactionRef, uuid.New().String(), tok.AccessToken)
if err != nil {
log.Err(err).Str("name", name).Msg("unable to resolve restaction, stopping")
encode.InternalError(wri, err)
return
}
}
log.Debug().Str("name", name).Msgf("values to replace: %s", additionalFieldstoReplace)
userinfo, err = updateConfig(userinfo, additionalFieldstoReplace)
Expand Down
14 changes: 10 additions & 4 deletions internal/routes/auth/oidc/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,16 @@ func (r *loginRoute) Handler() http.HandlerFunc {
log.Debug().Str("name", name).Msg("resolving restaction")
if cfg.RESTActionRef != nil {
additionalFieldstoReplace, err := restaction.Resolve(r.ctx, r.rc, cfg.RESTActionRef, idToken.email, idToken.bearerToken)
if err != nil {
log.Err(err).Str("name", name).Msg("unable to resolve restaction")
encode.InternalError(wri, err)
return
value, ok := additionalFieldstoReplace["name"]
_, okk := additionalFieldstoReplace["name"].(string)
if err != nil || !ok || !okk || value == nil {
log.Err(err).Str("name", name).Msg("unable to resolve restaction, retrying with legacy resolve (copy)")
additionalFieldstoReplace, err = restaction.LegacyResolve(r.ctx, r.rc, cfg.RESTActionRef, idToken.email, idToken.bearerToken)
if err != nil {
log.Err(err).Str("name", name).Msg("unable to resolve restaction, stopping")
encode.InternalError(wri, err)
return
}
}
log.Debug().Str("name", name).Msg("updating oidc idtoken")
log.Debug().Str("name", name).Msgf("old idToken - name: %s - preferredUsername: %s - email: %s - groups: %s - avatarURL: %s", idToken.name, idToken.preferredUsername, idToken.email, idToken.groups, idToken.avatarURL)
Expand Down
2 changes: 2 additions & 0 deletions testdata/oauth.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ spec:
headers:
- 'Accept: application/vnd.github+json'
- 'X-GitHub-Api-Version: 2022-11-28'
- "${ \"Authorization: Bearer \" + .token }"
path: "/user"
endpointRef:
name: github-api
Expand All @@ -49,6 +50,7 @@ spec:
verb: POST
headers:
- 'Content-Type: application/json'
- "${ \"Authorization: Bearer \" + .token }"
path: ""
payload: |
${ {query:"query{organization(login:\"krateoplatformops\"){teams(first:100,userLogins:[\"" + .userInfo.name + "\"]){edges{node{slug}}}}}"} }
Expand Down
98 changes: 98 additions & 0 deletions testdata/oidc-azure-pagination.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
---
apiVersion: v1
kind: Secret
metadata:
name: oidc-example-secret
namespace: demo-system
stringData:
clientSecret: <client-secret-provided-by-service>
---
apiVersion: "v1"
kind: Secret
metadata:
name: azure-entra
namespace: krateo-system
stringData:
server-url: https://graph.microsoft.com
---
apiVersion: oidc.authn.krateo.io/v1alpha1
kind: OIDCConfig
metadata:
name: oidc-example
namespace: demo-system
spec:
discoveryURL: https://<identity-provider-url>/.well-known/openid-configuration
# Use these three fields if you do not have a discovery endpoint
# authorizationURL: authorization endpoint
# tokenURL: token endpoint
# userInfoURL: userinfo endpoint
redirectURI: http://localhost:8080/auth?kind=oidc # While any redirect URI can be used, the Krateo frontend requires /auth?kind=oidc
clientID: <client-id-provided-by-service>
clientSecret:
name: oidc-example-secret
namespace: demo-system
key: clientSecret
additionalScopes: # e.g., "User.Read Directory.Read.All" for Azure
restActionRef: # optional
name: test-rest-action
namespace: krateo-system
graphics:
icon: "fa-brands fa-windows"
displayName: "Login with Azure"
backgroundColor: "#4444ff"
textColor: "#ffffff"
---
apiVersion: templates.krateo.io/v1
kind: RESTAction
metadata:
name: test-rest-action
namespace: krateo-system
spec:
api:
- name: groups
verb: GET
continueOnError: false
errorKey: groupsError
headers:
- 'Accept: application/json'
- "${ \"Authorization: Bearer \" + .token }"
path: ${ .next // "v1.0/me/memberOf?$select=displayName&" }
endpointRef:
name: azure-entra
namespace: krateo-system
filter: |
.groups | if has("@odata.nextLink") then
{
res: (.value | map(.displayName)),
next: (."@odata.nextLink" | sub("https://graph.microsoft.com"; ""))
}
else
{
res: (.value | map(.displayName))
}
end
- name: recursive
verb: GET
exportJwt: true
continueOnError: false
errorKey: recursiveError
dependsOn:
name: groups
iterator: |
.token as $token | .groups | if has("next") then [{next: .next, token: $token}] else [] end
headers:
- 'Accept: application/json'
path: |
${ "/call?apiVersion=templates.krateo.io/v1&resource=restactions&name=test-rest-action&namespace=krateo-system&extras=" + ({token: .token, next: .next } | tostring | @uri ) }
endpointRef:
name: snowplow-endpoint
namespace: krateo-system
filter: .recursive.status
filter: |
.groups.res as $groups |
.recursive |
if type == "object" then
.groups += $groups
elif type == "null" then
{"groups": $groups}
end
1 change: 1 addition & 0 deletions testdata/oidc-azure.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ spec:
verb: GET
headers:
- 'Accept: application/json'
- "${ \"Authorization: Bearer \" + .token }"
path: "v1.0/me/memberOf?$select=displayName"
endpointRef:
name: azure-entra
Expand Down