Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(organizations): organization SCIM config availability #648

Merged
merged 7 commits into from
Nov 12, 2024
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
39 changes: 39 additions & 0 deletions charts/manager/crds/greenhouse.sap_organizations.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,45 @@ spec:
type: object
status:
description: OrganizationStatus defines the observed state of an Organization
properties:
statusConditions:
description: StatusConditions contain the different conditions that
constitute the status of the Organization.
properties:
conditions:
items:
description: Condition contains additional information on the
state of a resource.
properties:
lastTransitionTime:
description: LastTransitionTime is the last time the condition
transitioned from one status to another.
format: date-time
type: string
message:
description: Message is an optional human readable message
indicating details about the last transition.
type: string
reason:
description: Reason is a one-word, CamelCase reason for
the condition's last transition.
type: string
status:
description: Status of the condition.
type: string
type:
description: Type of the condition.
type: string
required:
- lastTransitionTime
- status
- type
type: object
type: array
x-kubernetes-list-map-keys:
- type
x-kubernetes-list-type: map
type: object
type: object
type: object
served: true
Expand Down
2 changes: 1 addition & 1 deletion charts/manager/crds/greenhouse.sap_teammemberships.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ spec:
type: string
statusConditions:
description: StatusConditions contain the different conditions that
constitute the status of the TeamRoleBinding.
constitute the status of the TeamMembership.
properties:
conditions:
items:
Expand Down
30 changes: 29 additions & 1 deletion docs/reference/api/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -1366,6 +1366,33 @@ <h3 id="greenhouse.sap/v1alpha1.OrganizationStatus">OrganizationStatus
<a href="#greenhouse.sap/v1alpha1.Organization">Organization</a>)
</p>
<p>OrganizationStatus defines the observed state of an Organization</p>
<div class="md-typeset__scrollwrap">
<div class="md-typeset__table">
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>statusConditions</code><br>
<em>
<a href="#greenhouse.sap/v1alpha1.StatusConditions">
StatusConditions
</a>
</em>
</td>
<td>
<p>StatusConditions contain the different conditions that constitute the status of the Organization.</p>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<h3 id="greenhouse.sap/v1alpha1.Plugin">Plugin
</h3>
<p>Plugin is the Schema for the plugins API</p>
Expand Down Expand Up @@ -2591,6 +2618,7 @@ <h3 id="greenhouse.sap/v1alpha1.StatusConditions">StatusConditions
<a href="#greenhouse.sap/v1alpha1.ClusterKubeconfigStatus">ClusterKubeconfigStatus</a>,
<a href="#greenhouse.sap/v1alpha1.ClusterStatus">ClusterStatus</a>,
<a href="#greenhouse.sap/v1alpha1.NodeStatus">NodeStatus</a>,
<a href="#greenhouse.sap/v1alpha1.OrganizationStatus">OrganizationStatus</a>,
<a href="#greenhouse.sap/v1alpha1.PluginPresetStatus">PluginPresetStatus</a>,
<a href="#greenhouse.sap/v1alpha1.PluginStatus">PluginStatus</a>,
<a href="#greenhouse.sap/v1alpha1.TeamMembershipStatus">TeamMembershipStatus</a>,
Expand Down Expand Up @@ -2880,7 +2908,7 @@ <h3 id="greenhouse.sap/v1alpha1.TeamMembershipStatus">TeamMembershipStatus
</em>
</td>
<td>
<p>StatusConditions contain the different conditions that constitute the status of the TeamRoleBinding.</p>
<p>StatusConditions contain the different conditions that constitute the status of the TeamMembership.</p>
</td>
</tr>
</tbody>
Expand Down
38 changes: 36 additions & 2 deletions docs/reference/api/openapi.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
openapi: 3.0.0
info:
title: Greenhouse
version: 0fa6b52
version: c9d3eaa
description: PlusOne operations platform
paths:
/TeamMembership:
Expand Down Expand Up @@ -111,7 +111,7 @@ components:
format: date-time
type: string
statusConditions:
description: StatusConditions contain the different conditions that constitute the status of the TeamRoleBinding.
description: StatusConditions contain the different conditions that constitute the status of the TeamMembership.
properties:
conditions:
items:
Expand Down Expand Up @@ -455,6 +455,40 @@ components:
type: object
status:
description: OrganizationStatus defines the observed state of an Organization
properties:
statusConditions:
description: StatusConditions contain the different conditions that constitute the status of the Organization.
properties:
conditions:
items:
description: Condition contains additional information on the state of a resource.
properties:
lastTransitionTime:
description: LastTransitionTime is the last time the condition transitioned from one status to another.
format: date-time
type: string
message:
description: Message is an optional human readable message indicating details about the last transition.
type: string
reason:
description: Reason is a one-word, CamelCase reason for the condition's last transition.
type: string
status:
description: Status of the condition.
type: string
type:
description: Type of the condition.
type: string
required:
- lastTransitionTime
- status
- type
type: object
type: array
x-kubernetes-list-map-keys:
- type
x-kubernetes-list-type: map
type: object
type: object
type: object
PluginDefinition:
Expand Down
17 changes: 17 additions & 0 deletions docs/user-guides/organization/creation.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,24 @@ This ensures all pre-requisites are met, the organization is configured correctl
key: clientSecret
name: oidc-config
issuer: https://...
scim:
baseURL: URL to the SCIM server.
basicAuthUser:
secret:
name: Name of the secret in the same namespace.
key: Key in the secret holding the user value.
basicAuthPw:
secret:
name: Name of the secret in the same namespace.
key: Key in the secret holding the password value.
description: My new organization
displayName: Short name of the organization
mappedOrgAdminIdPGroup: Name of the group in the IDP that should be mapped to the organization admin role.
```

## Setting up Team Membership synchronization with Greenhouse
Team Membership synchronization with Greenhouse requires access to SCIM API.

For the Team Memberships to be created Organization needs to be configured with URL and credentials of the SCIM API. SCIM API is used to get members for teams in the organization based on the IDP groups set for teams.

IDP group for the organization admin team should be set to the `mappedOrgAdminIdPGroup` field in the Organization configuration. It is required for the synchronization to work. IDP groups for remaining teams in the organization should be set in their respective configurations.
7 changes: 5 additions & 2 deletions docs/user-guides/team/create.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@ While all members of an organization can see existing teams, their management re

## Creating a team

The team resource is used to structure members of your organization and assign fine-grained access and permission levels.
Each team must be backed by a group in the identity provider (IdP) of the organization.
The team resource is used to structure members of your organization and assign fine-grained access and permission levels.

Each Team must be backed by a group in the identity provider (IdP) of the Organization.
* IdP group should be set on the `mappedIdPGroup` field in Team configuration.
* This, along with SCIM API configured in the Organization, allows for synchronization of TeamMemberships with Greenhouse.

```
NOTE: The UI is currently in development. For now this guides describes the onboarding workflow via command line.
Expand Down
14 changes: 13 additions & 1 deletion pkg/apis/greenhouse/v1alpha1/organization_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

const (
// SCIMAPIAvailableCondition reflects if there is a connection to SCIM API.
SCIMAPIAvailableCondition ConditionType = "SCIMAPIAvailable"
// SecretNotFoundReason is set when the secret with credentials to SCIM is not found.
SecretNotFoundReason ConditionReason = "SecretNotFound"
// SCIMRequestFailedReason is set when a request to SCIM failed.
SCIMRequestFailedReason ConditionReason = "SCIMRequestFailed"
)

// OrganizationSpec defines the desired state of Organization
type OrganizationSpec struct {
// DisplayName is an optional name for the organization to be displayed in the Greenhouse UI.
Expand Down Expand Up @@ -52,7 +61,10 @@ type SCIMConfig struct {
}

// OrganizationStatus defines the observed state of an Organization
type OrganizationStatus struct{}
type OrganizationStatus struct {
// StatusConditions contain the different conditions that constitute the status of the Organization.
StatusConditions `json:"statusConditions,omitempty"`
}

//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
Expand Down
12 changes: 5 additions & 7 deletions pkg/apis/greenhouse/v1alpha1/teammembership_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,10 @@ import (
)

const (
// ScimAccessReadyCondition reflects if there is a connection to SCIM.
ScimAccessReadyCondition ConditionType = "ScimAccessReady"
// SecretNotFoundReason is set when the secret with credentials to SCIM is not found.
SecretNotFoundReason ConditionReason = "SecretNotFound"
// ScimRequestFailedReason is set when a request to SCIM failed.
ScimRequestFailedReason ConditionReason = "ScimRequestFailed"
// SCIMAccessReadyCondition reflects if there is a connection to SCIM.
SCIMAccessReadyCondition ConditionType = "SCIMAccessReady"
// SCIMAPIUnavailableReason is set when the organization has set SCIMAPIAvailableCondition to false.
SCIMAPIUnavailableReason ConditionReason = "SCIMAPIUnavailable"
)

// User specifies a human person.
Expand Down Expand Up @@ -43,7 +41,7 @@ type TeamMembershipStatus struct {
// LastChangedTime is the information when was the last time the membership was actually changed
// +optional
LastChangedTime *metav1.Time `json:"lastUpdateTime,omitempty"`
// StatusConditions contain the different conditions that constitute the status of the TeamRoleBinding.
// StatusConditions contain the different conditions that constitute the status of the TeamMembership.
StatusConditions `json:"statusConditions,omitempty"`
}

Expand Down
3 changes: 2 additions & 1 deletion pkg/apis/greenhouse/v1alpha1/zz_generated.deepcopy.go

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

88 changes: 88 additions & 0 deletions pkg/controllers/organization/organization_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@ import (
greenhousesapv1alpha1 "github.com/cloudoperators/greenhouse/pkg/apis/greenhouse/v1alpha1"
"github.com/cloudoperators/greenhouse/pkg/clientutil"
"github.com/cloudoperators/greenhouse/pkg/lifecycle"
"github.com/cloudoperators/greenhouse/pkg/scim"
)

var (
// exposedConditions are the conditions that are exposed in the StatusConditions of the Organization.
exposedConditions = []greenhousesapv1alpha1.ConditionType{
greenhousesapv1alpha1.ReadyCondition,
greenhousesapv1alpha1.SCIMAPIAvailableCondition,
}
)

// OrganizationReconciler reconciles an Organization object
Expand Down Expand Up @@ -58,10 +67,21 @@ func (r *OrganizationReconciler) EnsureCreated(ctx context.Context, object lifec
return ctrl.Result{}, lifecycle.Failed, errors.Errorf("RuntimeObject has incompatible type.")
}

orgStatus := initOrganizationStatus(org)
defer func() {
if statusErr := r.setStatus(ctx, org, orgStatus); statusErr != nil {
log.FromContext(ctx).Error(statusErr, "failed to set status")
}
}()

if err := r.reconcileNamespace(ctx, org); err != nil {
return ctrl.Result{}, lifecycle.Failed, err
}

scimAPIAvailableCondition := r.checkSCIMAPIAvailability(ctx, org)
readyCondition := calculateReadyCondition(scimAPIAvailableCondition)
orgStatus.SetConditions(scimAPIAvailableCondition, readyCondition)

if err := r.reconcileAdminTeam(ctx, org); err != nil {
return ctrl.Result{}, lifecycle.Failed, err
}
Expand Down Expand Up @@ -115,3 +135,71 @@ func (r *OrganizationReconciler) reconcileAdminTeam(ctx context.Context, org *gr
}
return nil
}

func (r *OrganizationReconciler) checkSCIMAPIAvailability(ctx context.Context, org *greenhousesapv1alpha1.Organization) greenhousesapv1alpha1.Condition {
if org.Spec.Authentication == nil || org.Spec.Authentication.SCIMConfig == nil {
// SCIM Config is optional.
return greenhousesapv1alpha1.UnknownCondition(greenhousesapv1alpha1.SCIMAPIAvailableCondition, "", "SCIM Config not provided")
}

if org.Spec.MappedOrgAdminIDPGroup == "" {
return greenhousesapv1alpha1.FalseCondition(greenhousesapv1alpha1.SCIMAPIAvailableCondition, greenhousesapv1alpha1.SCIMRequestFailedReason, ".Spec.MappedOrgAdminIDPGroup is not set in Organization")
}

namespace := org.Name
scimConfig := org.Spec.Authentication.SCIMConfig

basicAuthUser, err := clientutil.GetSecretKeyFromSecretKeyReference(ctx, r.Client, namespace, *scimConfig.BasicAuthUser.Secret)
if err != nil {
return greenhousesapv1alpha1.FalseCondition(greenhousesapv1alpha1.SCIMAPIAvailableCondition, greenhousesapv1alpha1.SecretNotFoundReason, "BasicAuthUser missing")
}
basicAuthPw, err := clientutil.GetSecretKeyFromSecretKeyReference(ctx, r.Client, namespace, *scimConfig.BasicAuthPw.Secret)
if err != nil {
return greenhousesapv1alpha1.FalseCondition(greenhousesapv1alpha1.SCIMAPIAvailableCondition, greenhousesapv1alpha1.SecretNotFoundReason, "BasicAuthPw missing")
}
clientConfig := scim.Config{
RawURL: scimConfig.BaseURL,
AuthType: scim.Basic,
BasicAuthConfig: &scim.BasicAuthConfig{
BasicAuthUser: basicAuthUser,
BasicAuthPw: basicAuthPw,
},
}
scimClient, err := scim.NewScimClient(clientConfig)
if err != nil {
return greenhousesapv1alpha1.FalseCondition(greenhousesapv1alpha1.SCIMAPIAvailableCondition, greenhousesapv1alpha1.SCIMRequestFailedReason, "Failed to create SCIM client")
}

_, err = scimClient.GetTeamMembers(org.Spec.MappedOrgAdminIDPGroup)
if err != nil {
return greenhousesapv1alpha1.FalseCondition(greenhousesapv1alpha1.SCIMAPIAvailableCondition, greenhousesapv1alpha1.SCIMRequestFailedReason, "Failed to request data from SCIM API")
}

return greenhousesapv1alpha1.TrueCondition(greenhousesapv1alpha1.SCIMAPIAvailableCondition, "", "")
}

func calculateReadyCondition(scimAPIAvailableCondition greenhousesapv1alpha1.Condition) greenhousesapv1alpha1.Condition {
if scimAPIAvailableCondition.IsFalse() {
return greenhousesapv1alpha1.FalseCondition(greenhousesapv1alpha1.ReadyCondition, greenhousesapv1alpha1.SCIMAPIUnavailableReason, "")
}
// If SCIM API availability is unknown, then Ready state should be True, because SCIM Config is optional.
return greenhousesapv1alpha1.TrueCondition(greenhousesapv1alpha1.ReadyCondition, "", "")
}

func initOrganizationStatus(org *greenhousesapv1alpha1.Organization) greenhousesapv1alpha1.OrganizationStatus {
orgStatus := org.Status.DeepCopy()
for _, t := range exposedConditions {
if orgStatus.GetConditionByType(t) == nil {
orgStatus.SetConditions(greenhousesapv1alpha1.UnknownCondition(t, "", ""))
}
}
return *orgStatus
}

func (r *OrganizationReconciler) setStatus(ctx context.Context, org *greenhousesapv1alpha1.Organization, orgStatus greenhousesapv1alpha1.OrganizationStatus) error {
_, err := clientutil.PatchStatus(ctx, r.Client, org, func() error {
org.Status = orgStatus
return nil
})
return err
}
Loading