Skip to content
Closed
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
8 changes: 7 additions & 1 deletion appconfig/default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ pattern:
- input: "^([^\\_]+)$"
Comment thread
vinamra28 marked this conversation as resolved.
output: "$1"


app:
name: "usernaut"
version: "0.0.1"
Expand Down Expand Up @@ -81,3 +80,10 @@ backends:
cert_path: "/path/to/usernaut.crt"
private_key_path: "/path/to/usernaut.pem"
service_account_name: test-service-account
- name: atlan
type: "atlan"
enabled: true
connection:
url: "https://yourcompany.atlan.com"
api_token: file|path/to/api_token
identity_provider_alias: {saml|azure} # REQUIRED for SSO-Group mapping functions
17 changes: 17 additions & 0 deletions internal/controller/group_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,22 @@ func (r *GroupReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl
"backend_type": backend.Type,
})

// ATLAN VALIDATION: Check if Rover backend exists in current CR before creating Atlan team
if backend.Type == "atlan" {
roverExists := false
for _, b := range groupCR.Spec.Backends {
if b.Type == "rover" {
roverExists = true
break
}
}

if !roverExists {
r.backendLogger.Error("cannot process Atlan backend: Rover backend must exist in the same Group CR")
return ctrl.Result{}, errors.New("cannot process Atlan backend: Rover backend must exist in the same Group CR")
}
}

// process each backend in the group CR
backendClient, err := clients.New(backend.Name, backend.Type, r.AppConfig.BackendMap)
if err != nil {
Expand Down Expand Up @@ -310,6 +326,7 @@ func (r *GroupReconciler) fetchOrCreateTeam(ctx context.Context,
return teamID, nil
}
}

// If team details are not found in cache, create a new team
r.backendLogger.Info("team details not found in cache, creating a new team")

Expand Down
118 changes: 118 additions & 0 deletions pkg/clients/atlan/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
Copyright 2025.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package atlan

import (
"context"
"encoding/json"
"fmt"
"net/http"
"slices"
"time"

"github.com/gojek/heimdall/v7"
"github.com/redhat-data-and-ai/usernaut/pkg/request"
"github.com/redhat-data-and-ai/usernaut/pkg/request/httpclient"
"github.com/redhat-data-and-ai/usernaut/pkg/utils"
)

// AtlanClient is a simple HTTP client for Atlan API
type AtlanClient struct {
client heimdall.Doer
url string
apiToken string
identityProviderAlias string
}

// AtlanConfig holds the configuration needed to connect to Atlan
type AtlanConfig struct {
URL string `json:"url"`
APIToken string `json:"api_token"`
IdentityProviderAlias string `json:"identity_provider_alias"`
}

// NewClient creates a new Atlan client with simple API token authentication
func NewClient(atlanAppConfig map[string]interface{},
connectionPoolConfig httpclient.ConnectionPoolConfig,
hystrixResiliencyConfig httpclient.HystrixResiliencyConfig) (*AtlanClient, error) {

atlanConfig := AtlanConfig{}
if err := utils.MapToStruct(atlanAppConfig, &atlanConfig); err != nil {
return nil, fmt.Errorf("failed to parse atlan configuration: %w", err)
}

// Validate required fields
if atlanConfig.URL == "" {
return nil, fmt.Errorf("atlan configuration is missing required field: URL")
}
if atlanConfig.APIToken == "" {
return nil, fmt.Errorf("atlan configuration is missing required field: APIToken")
}

// Initialize HTTP client without certificates (Atlan uses API token, not certs)
client, err := httpclient.InitializeClient(
"atlan",
connectionPoolConfig,
hystrixResiliencyConfig,
heimdall.NewRetrier(heimdall.NewConstantBackoff(100*time.Millisecond, 50*time.Millisecond)), // retry logic
3,
nil)
if err != nil {
return nil, fmt.Errorf("failed to initialize http client: %w", err)
}

return &AtlanClient{
client: client,
url: atlanConfig.URL,
apiToken: atlanConfig.APIToken,
identityProviderAlias: atlanConfig.IdentityProviderAlias,
}, nil
}

// sendRequest makes an HTTP request to the Atlan API with proper authentication
func (aC *AtlanClient) sendRequest(ctx context.Context, url string, method string, body interface{},
headers map[string]string, methodName string) ([]byte, int, error) {
requestBody, err := json.Marshal(body)
if err != nil {
return nil, 0, fmt.Errorf("failed to marshal request body: %w", err)
}

req, err := request.NewRequest(ctx, method, url, requestBody)
if err != nil {
return nil, 0, fmt.Errorf("failed to create request: %w", err)
}

if headers == nil {
headers = make(map[string]string)
}
headers["Authorization"] = "Bearer " + aC.apiToken
headers["Content-Type"] = "application/json"
headers["Accept"] = "application/json"

req.SetHeaders(headers)

response, statusCode, err := req.MakeRequest(aC.client, methodName, "atlan")
if err != nil {
return nil, statusCode, fmt.Errorf("request failed: %w", err)
}

if !slices.Contains([]int{http.StatusOK, http.StatusCreated, http.StatusNoContent}, statusCode) {
return nil, statusCode, fmt.Errorf("unexpected status code: %d", statusCode)
}

return response, statusCode, nil
}
22 changes: 22 additions & 0 deletions pkg/clients/atlan/team_membership.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package atlan

import (
"context"

"github.com/redhat-data-and-ai/usernaut/pkg/common/structs"
)

func (ac *AtlanClient) FetchTeamMembersByTeamID(ctx context.Context, teamID string) (map[string]*structs.User, error) {
// Team membership is synced via LDAP, returning empty map
return make(map[string]*structs.User), nil
}

func (ac *AtlanClient) AddUserToTeam(ctx context.Context, teamID, userID string) error {
// Team membership is synced via LDAP, returning nil
return nil
}

func (ac *AtlanClient) RemoveUserFromTeam(ctx context.Context, teamID, userID string) error {
// Team membership is synced via LDAP, returning nil
return nil
}
Loading
Loading