From 9280b4ebb40d41efbfadb378d8160dde1e7553fe Mon Sep 17 00:00:00 2001 From: Wouter Cordewiner <73988312+wouterc-collibra@users.noreply.github.com> Date: Fri, 8 May 2026 16:33:40 +0200 Subject: [PATCH 01/13] Tools for Data Access --- SKILLS.md | 30 +- go.mod | 16 +- go.sum | 41 ++- pkg/clients/data_access_client.go | 350 +++++++++++++++++++ pkg/tools/get_data_access_control_details.go | 46 +++ pkg/tools/search_data_access_controls.go | 81 +++++ pkg/tools/search_data_access_identities.go | 48 +++ pkg/tools/tools_register.go | 4 + 8 files changed, 600 insertions(+), 16 deletions(-) create mode 100644 pkg/clients/data_access_client.go create mode 100644 pkg/tools/get_data_access_control_details.go create mode 100644 pkg/tools/search_data_access_controls.go create mode 100644 pkg/tools/search_data_access_identities.go diff --git a/SKILLS.md b/SKILLS.md index e4cc2c4..c141437 100644 --- a/SKILLS.md +++ b/SKILLS.md @@ -67,6 +67,18 @@ These tools query the technical lineage graph — a map of all data objects and **`get_lineage_transformation`** — Get the full details of a transformation, including its SQL or script logic. Use after finding a transformation ID in an upstream/downstream result or search. +### Data Access + +These tools query Collibra Data Access — the system that manages who can access what data, through grants, masks, filters, and groups. + +**`search_data_access_controls`** — Search for data access controls. All filters are optional and combinable: `name` (case-insensitive contains), `actions` (one or more of `Grant`, `Mask`, `Filter`, `Share`, `Group`, `FilterRule`), `states` (one or more of `Active`, `Inactive`, `Deleted`). Returns a paginated list (25 per page); pass the returned `nextCursor` to fetch subsequent pages. + +**`search_data_access_roles`** — Alias of `search_data_access_controls` restricted to `Grant`-type controls. Use this when the user asks specifically about roles or who has been granted access. Supports the same `name` and `states` filters; the `actions` filter is fixed to `Grant` and cannot be overridden. Returns a paginated list (25 per page); pass the returned `nextCursor` to fetch subsequent pages. + +**`get_data_access_control_details`** — Retrieve full details for a single data access control by its id. Use this when you already have an access control ID and need to inspect it. + +**`search_data_access_identities`** — Search for Data Access users (identities) by name and/or email. Providing `email` performs an exact lookup via `GetUserByEmail`. Providing `name` performs a server-side case-insensitive contains search via `SearchUsers`. Both can be combined: email resolves the user, name filters the result client-side. Name-only searches are paginated (25 per page) — use the returned `nextCursor` to fetch subsequent pages. + ### Data Contracts **`list_data_contract`** — List data contracts with cursor-based pagination. Filter by `manifestId`. Use this to find a contract's UUID. @@ -111,6 +123,22 @@ These tools query the technical lineage graph — a map of all data objects and 2. `get_lineage_downstream` → relations with consumer entity IDs 3. Follow up with `get_lineage_entity` for specific consumers as needed +### Find and inspect data access controls +1. `search_data_access_controls` with optional name/action/state filters → get matching controls and their IDs +2. `get_data_access_control_details` with a specific ID → full details including grant category, policy rule, timestamps + +### Find and inspect data access roles +1. `search_data_access_roles` with optional name/state filters → get matching controls and their IDs +2. `get_data_access_control_details` with a specific ID → full details including grant category, policy rule, timestamps + +### Find who has been granted access (roles) +1. `search_data_access_roles` with optional name/state filters → returns only Grant-type controls +2. `get_data_access_control_details` for any result ID → full grant details + +### Look up a Data Access user by email or name +1. `search_data_access_identities` with `email` → exact lookup, returns the user's id, display name, and type + — or with `name` → paginated server-side contains search across all users + ### Manage a data contract 1. `list_data_contract` to find the contract UUID 2. `pull_data_contract_manifest` to download, edit, then `push_data_contract_manifest` to update @@ -122,5 +150,5 @@ These tools query the technical lineage graph — a map of all data objects and - **UUIDs are required for most tools.** When you only have a name, start with `search_asset_keyword` or the natural language discovery tools to get the UUID first. - **`discover_data_assets` vs `search_asset_keyword`**: Prefer `discover_data_assets` for open-ended semantic questions; prefer `search_asset_keyword` when you know the exact name or need to filter by type/community/domain. - **Permissions**: `discover_data_assets` and `discover_business_glossary` require the `dgc.ai-copilot` permission. Classification tools require `dgc.classify` + `dgc.catalog`. If a tool fails with a permission error, let the user know which permission is needed. -- **Pagination**: `search_asset_keyword`, `list_asset_types`, `search_data_class`, and `search_data_classification_match` use `limit`/`offset`. `list_data_contract` and `get_asset_details` (for relations) use cursor-based pagination — carry the cursor from the previous response. Lineage tools (`search_lineage_entities`, `get_lineage_upstream`, `get_lineage_downstream`, `search_lineage_transformations`) also use cursor-based pagination. +- **Pagination**: `search_asset_keyword`, `list_asset_types`, `search_data_class`, and `search_data_classification_match` use `limit`/`offset`. `list_data_contract` and `get_asset_details` (for relations) use cursor-based pagination — carry the cursor from the previous response. Lineage tools (`search_lineage_entities`, `get_lineage_upstream`, `get_lineage_downstream`, `search_lineage_transformations`) and data access tools (`search_data_access_controls`, `search_data_access_roles`) also use cursor-based pagination. - **Error handling**: Validation errors are returned in the output `error` field (not as Go errors), so always check `error` and `success`/`found` fields in the response before using the data. diff --git a/go.mod b/go.mod index cc85499..a893e18 100644 --- a/go.mod +++ b/go.mod @@ -1,29 +1,37 @@ module github.com/collibra/chip -go 1.25.0 +go 1.26.2 require ( + github.com/collibra/data-access-go-sdk v0.0.0-00010101000000-000000000000 github.com/google/go-querystring v1.1.0 + github.com/google/jsonschema-go v0.3.0 github.com/google/uuid v1.6.0 github.com/modelcontextprotocol/go-sdk v1.1.0 github.com/spf13/pflag v1.0.10 github.com/spf13/viper v1.21.0 ) +replace github.com/collibra/data-access-go-sdk => /Users/wouterc/w/data-access-go-sdk-mcp + require ( + github.com/Khan/genqlient v0.8.2-0.20251028054717-8ddeeee0a15a // indirect + github.com/Masterminds/semver/v3 v3.5.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect - github.com/google/jsonschema-go v0.3.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-retryablehttp v0.7.8 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/sagikazarmark/locafero v0.12.0 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/vektah/gqlparser/v2 v2.5.30 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/oauth2 v0.34.0 // indirect - golang.org/x/sys v0.39.0 // indirect + golang.org/x/sys v0.42.0 // indirect golang.org/x/text v0.32.0 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 2e29b79..cbe86ab 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,13 @@ -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/Khan/genqlient v0.8.2-0.20251028054717-8ddeeee0a15a h1:kx/iDWW6lRgNBqTL1z6UB7ajcZR3OcVLKVJ39QkUnUw= +github.com/Khan/genqlient v0.8.2-0.20251028054717-8ddeeee0a15a/go.mod h1:guUHwMi8ByjIvs3TyAPM+V9ryaW305CtK7+aCeP2Jzc= +github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= +github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= @@ -15,23 +23,32 @@ github.com/google/jsonschema-go v0.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIy github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= +github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/modelcontextprotocol/go-sdk v1.1.0 h1:Qjayg53dnKC4UZ+792W21e4BpwEZBzwgRW6LrjLWSwA= github.com/modelcontextprotocol/go-sdk v1.1.0/go.mod h1:6fM3LCm3yV7pAs8isnKLn07oKtB0MP9LHd3DfAcKw10= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= +github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= @@ -44,18 +61,20 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/vektah/gqlparser/v2 v2.5.30 h1:EqLwGAFLIzt1wpx1IPpY67DwUujF1OfzgEyDsLrN6kE= +github.com/vektah/gqlparser/v2 v2.5.30/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= -golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= -golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= -golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/pkg/clients/data_access_client.go b/pkg/clients/data_access_client.go new file mode 100644 index 0000000..4bff19a --- /dev/null +++ b/pkg/clients/data_access_client.go @@ -0,0 +1,350 @@ +package clients + +import ( + "context" + "errors" + "fmt" + "net/http" + "strings" + "time" + + "github.com/collibra/chip/pkg/chip" + sdk "github.com/collibra/data-access-go-sdk" + "github.com/collibra/data-access-go-sdk/services" + "github.com/collibra/data-access-go-sdk/types" +) + +// DataAccessControlDetails holds the details of a single data access control. +type DataAccessControlDetails struct { + ID string `json:"id" jsonschema:"Unique identifier of the access control"` + Name string `json:"name" jsonschema:"Name of the access control"` + Description string `json:"description" jsonschema:"Detailed description of the access control"` + State string `json:"state" jsonschema:"State of the access control: ACTIVE, INACTIVE, or DELETED"` + Action string `json:"action" jsonschema:"Action type of the access control: GRANT, MASK, FILTER, SHARE, GROUP, or FILTERRULE"` + Category *DataAccessGrantCategory `json:"category,omitempty" jsonschema:"Grant category details, present only for GRANT action type"` + External bool `json:"external" jsonschema:"Whether the access control is managed externally in the data source rather than in Collibra Data Access"` + NamingHint *string `json:"namingHint,omitempty" jsonschema:"Naming hint used for generating names in target systems"` + PolicyRule *string `json:"policyRule,omitempty" jsonschema:"Policy rule string, used for imported row-level filters and column masks"` + NotInternalizable bool `json:"notInternalizable" jsonschema:"Whether the external access control cannot be internalized"` + Complete *bool `json:"complete,omitempty" jsonschema:"Whether the external access control is complete (all linked entities known in Collibra Data Access)"` + WhatUnknown bool `json:"whatUnknown" jsonschema:"Whether the WHAT scope of this access control could not be parsed on import"` + WhoUnknown bool `json:"whoUnknown" jsonschema:"Whether the WHO scope of this access control could not be parsed on import"` + CreatedAt time.Time `json:"createdAt" jsonschema:"Timestamp when the access control was created"` + ModifiedAt time.Time `json:"modifiedAt" jsonschema:"Timestamp when the access control was last modified"` + What []DataAccessWhatItem `json:"what" jsonschema:"List of access controls that this control applies to (the WHAT scope)"` + Who []DataAccessWhoItem `json:"who" jsonschema:"List of principals (users, access controls, data sources) that are granted access by this control"` + SyncData []DataAccessSyncData `json:"syncData" jsonschema:"Synchronization status per linked data source. Valid sync statuses: Notconnected, Failed, Outofdate, Inprogress, Synced, Outofsync."` +} + +// DataAccessSyncData holds the sync status of an access control for a single data source. +type DataAccessSyncData struct { + DataSourceID string `json:"dataSourceId" jsonschema:"Unique identifier of the linked data source"` + DataSourceName string `json:"dataSourceName" jsonschema:"Name of the linked data source"` + SyncStatus string `json:"syncStatus" jsonschema:"Sync status for this data source. Valid values: Notconnected, Failed, Outofdate, Inprogress, Synced, Outofsync."` +} + +// DataAccessWhatItem represents a single entry in the WHAT list of an access control — +// another access control that this one applies to. +type DataAccessWhatItem struct { + ID string `json:"id" jsonschema:"Unique identifier of the access control in the WHAT list"` + Name string `json:"name" jsonschema:"Name of the access control in the WHAT list"` + State string `json:"state" jsonschema:"State of the access control: Active, Inactive, or Deleted"` + Action string `json:"action" jsonschema:"Action type of the access control: Grant, Mask, Filter, Share, Group, or FilterRule"` + ExpiresAt *time.Time `json:"expiresAt,omitempty" jsonschema:"Optional expiration time for this WHAT entry"` +} + +// DataAccessWhoItem represents a single entry in the WHO list of an access control. +type DataAccessWhoItem struct { + // Type is either "WhoGrant" (direct access) or "WhoPromise" (pre-approved access on request). + Type string `json:"type" jsonschema:"Grant type: WhoGrant (direct access) or WhoPromise (pre-approved on request)"` + ExpiresAt *time.Time `json:"expiresAt,omitempty" jsonschema:"Optional expiration time for this WHO entry"` + PromiseDuration *int64 `json:"promiseDuration,omitempty" jsonschema:"For WhoPromise: duration in seconds of the grant when access is requested"` + // ItemType is the GraphQL typename of the item: User, AccessControl, DataShareRecipient, DataSource. + ItemType string `json:"itemType" jsonschema:"Type of the granted principal: User, AccessControl, DataShareRecipient, or DataSource"` + ItemID string `json:"itemId,omitempty" jsonschema:"ID of the granted principal (present for User and AccessControl item types)"` + ItemName string `json:"itemName,omitempty" jsonschema:"Display name of the granted principal (present for User and AccessControl item types)"` + Email *string `json:"email,omitempty" jsonschema:"Email address of the user (present for User item type only)"` + UserType string `json:"userType,omitempty" jsonschema:"Whether the user is a Human or Machine user (present for User item type only)"` +} + +// DataAccessGrantCategory holds the details of a grant category. +type DataAccessGrantCategory struct { + ID string `json:"id" jsonschema:"Unique identifier of the grant category"` + Name string `json:"name" jsonschema:"Display name of the grant category"` + NamePlural string `json:"namePlural" jsonschema:"Plural display name of the grant category"` + IsSystem bool `json:"isSystem" jsonschema:"Whether this grant category is system-defined and cannot be edited or removed"` + IsDefault bool `json:"isDefault" jsonschema:"Whether this is the default grant category for new access controls"` +} + +// GetDataAccessControl retrieves a single data access control by ID. +// It creates an sdk.CollibraClient using chip's existing HTTP client via sdk.WithHTTPClient, +// so URL routing and authentication are handled by chip's RoundTripper. +func GetDataAccessControl(ctx context.Context, httpClient *http.Client, id string) (*DataAccessControlDetails, error) { + collibraHost, ok := chip.GetCollibraHost(ctx) + if !ok { + return nil, fmt.Errorf("collibra host not configured in context") + } + dataAccessURL := strings.TrimSuffix(collibraHost, "/") + "/dataAccess" + + collibraClient, err := sdk.NewClient(dataAccessURL, sdk.WithHTTPClient(httpClient)) + if err != nil { + return nil, fmt.Errorf("failed to create data access client: %w", err) + } + + accessControlClient := collibraClient.AccessControl() + + ac, err := accessControlClient.GetAccessControl(ctx, id) + if err != nil { + return nil, err + } + + details := mapToDataAccessControlDetails(ac) + + for whatItem, err := range accessControlClient.GetAccessControlWhatAccessControlList(ctx, id) { + if err != nil { + return nil, fmt.Errorf("failed to retrieve what list: %w", err) + } + details.What = append(details.What, mapToDataAccessWhatItem(whatItem)) + } + + for whoItem, err := range accessControlClient.GetAccessControlWhoList(ctx, id) { + if err != nil { + return nil, fmt.Errorf("failed to retrieve who list: %w", err) + } + details.Who = append(details.Who, mapToDataAccessWhoItem(whoItem)) + } + + return details, nil +} + +// SearchDataAccessControlsResult holds a page of access controls and an optional next-page cursor. +type SearchDataAccessControlsResult struct { + Items []*DataAccessControlDetails `json:"items"` + NextCursor *string `json:"nextCursor,omitempty"` +} + +// SearchDataAccessControls returns a page of data access controls filtered by name, actions, and/or states. +// Name search is case-insensitive contains. Pass cursor from a previous response to fetch the next page. +func SearchDataAccessControls(ctx context.Context, httpClient *http.Client, name string, actions []string, states []string, cursor string, pageSize int) (*SearchDataAccessControlsResult, error) { + collibraHost, ok := chip.GetCollibraHost(ctx) + if !ok { + return nil, fmt.Errorf("collibra host not configured in context") + } + dataAccessURL := strings.TrimSuffix(collibraHost, "/") + "/dataAccess" + + collibraClient, err := sdk.NewClient(dataAccessURL, sdk.WithHTTPClient(httpClient)) + if err != nil { + return nil, fmt.Errorf("failed to create data access client: %w", err) + } + + filter := &types.AccessControlFilterInput{} + if name != "" { + filter.Search = &name + } + for _, a := range actions { + filter.Actions = append(filter.Actions, types.AccessControlAction(a)) + } + for _, s := range states { + filter.States = append(filter.States, types.AccessControlState(s)) + } + + opts := []func(*services.AccessControlListOptions){ + services.WithAccessControlListFilter(filter), + } + if cursor != "" { + opts = append(opts, services.WithAccessControlListCursor(cursor)) + } + if pageSize > 0 { + opts = append(opts, services.WithAccessControlListPageSize(pageSize)) + } + + items, nextCursor, err := collibraClient.AccessControl().ListAccessControlsPage(ctx, opts...) + if err != nil { + return nil, err + } + + result := &SearchDataAccessControlsResult{ + Items: make([]*DataAccessControlDetails, 0, len(items)), + NextCursor: nextCursor, + } + for _, ac := range items { + result.Items = append(result.Items, mapToDataAccessControlDetails(ac)) + } + return result, nil +} + +func mapToDataAccessWhatItem(w *types.AccessWhatAccessControlItem) DataAccessWhatItem { + item := DataAccessWhatItem{ + ExpiresAt: w.ExpiresAt, + } + if w.AccessControl != nil { + item.ID = w.AccessControl.AccessControl.Id + item.Name = w.AccessControl.AccessControl.Name + item.State = string(w.AccessControl.AccessControl.State) + item.Action = string(w.AccessControl.AccessControl.Action) + } + return item +} + +func mapToDataAccessWhoItem(w *types.AccessWhoItem) DataAccessWhoItem { + item := DataAccessWhoItem{ + Type: string(w.Type), + ExpiresAt: w.ExpiresAt, + PromiseDuration: w.PromiseDuration, + } + + switch v := w.Item.(type) { + case *types.AccessWhoItemItemUser: + item.ItemType = "User" + item.ItemID = v.User.Id + item.ItemName = v.User.Name + item.Email = v.User.Email + item.UserType = string(v.User.Type) + case *types.AccessWhoItemItemAccessControl: + item.ItemType = "AccessControl" + item.ItemID = v.Id + item.ItemName = v.Name + case *types.AccessWhoItemItemDataShareRecipient: + item.ItemType = "DataShareRecipient" + case *types.AccessWhoItemItemDataSource: + item.ItemType = "DataSource" + default: + if v != nil { + if t := w.Item.GetTypename(); t != nil { + item.ItemType = *t + } + } + } + + return item +} + +// DataAccessIdentity represents a user in Collibra Data Access. +type DataAccessIdentity struct { + ID string `json:"id" jsonschema:"Unique identifier of the user"` + Name string `json:"name" jsonschema:"Display name of the user"` + Email *string `json:"email,omitempty" jsonschema:"Email address of the user"` + Type string `json:"type" jsonschema:"User type: Human or Machine"` +} + +// SearchDataAccessIdentitiesResult holds a page of identities and an optional next-page cursor. +type SearchDataAccessIdentitiesResult struct { + Items []*DataAccessIdentity + NextCursor *string +} + +// SearchDataAccessIdentities searches for Data Access users by name and/or email. +// When email is provided, an exact lookup via GetUserByEmail is performed. Name is then applied +// as an optional client-side case-insensitive contains filter on the result. +// When only name is provided, SearchUsers is called with the Search filter (case-insensitive +// contains). Cursor and pageSize control pagination for name-based searches. +func SearchDataAccessIdentities(ctx context.Context, httpClient *http.Client, name, email, cursor string, pageSize int) (*SearchDataAccessIdentitiesResult, error) { + collibraHost, ok := chip.GetCollibraHost(ctx) + if !ok { + return nil, fmt.Errorf("collibra host not configured in context") + } + dataAccessURL := strings.TrimSuffix(collibraHost, "/") + "/dataAccess" + + collibraClient, err := sdk.NewClient(dataAccessURL, sdk.WithHTTPClient(httpClient)) + if err != nil { + return nil, fmt.Errorf("failed to create data access client: %w", err) + } + + if email != "" { + user, err := collibraClient.User().GetUserByEmail(ctx, email) + if err != nil { + var notFound *types.ErrNotFound + if errors.As(err, ¬Found) { + return &SearchDataAccessIdentitiesResult{Items: []*DataAccessIdentity{}}, nil + } + return nil, err + } + + identity := mapToDataAccessIdentity(user) + if name != "" && !strings.Contains(strings.ToLower(identity.Name), strings.ToLower(name)) { + return &SearchDataAccessIdentitiesResult{Items: []*DataAccessIdentity{}}, nil + } + return &SearchDataAccessIdentitiesResult{Items: []*DataAccessIdentity{identity}}, nil + } + + // Name-only path: use SearchUsers with the Search filter. + filter := &types.UserFilterInput{} + if name != "" { + filter.Search = &name + } + + var after *string + if cursor != "" { + after = &cursor + } + var limit *int + if pageSize > 0 { + limit = &pageSize + } + + users, nextCursor, err := collibraClient.User().SearchUsers(ctx, after, limit, filter) + if err != nil { + return nil, err + } + + result := &SearchDataAccessIdentitiesResult{ + Items: make([]*DataAccessIdentity, 0, len(users)), + NextCursor: nextCursor, + } + for i := range users { + result.Items = append(result.Items, mapToDataAccessIdentity(&users[i])) + } + return result, nil +} + +func mapToDataAccessIdentity(u *types.User) *DataAccessIdentity { + return &DataAccessIdentity{ + ID: u.Id, + Name: u.Name, + Email: u.Email, + Type: string(u.Type), + } +} + +func mapToDataAccessControlDetails(ac *types.AccessControl) *DataAccessControlDetails { + details := &DataAccessControlDetails{ + What: []DataAccessWhatItem{}, + Who: []DataAccessWhoItem{}, + SyncData: []DataAccessSyncData{}, + ID: ac.Id, + Name: ac.Name, + Description: ac.Description, + State: string(ac.State), + Action: string(ac.Action), + External: ac.External, + NamingHint: ac.NamingHint, + PolicyRule: ac.PolicyRule, + NotInternalizable: ac.NotInternalizable, + Complete: ac.Complete, + WhatUnknown: ac.WhatUnknown, + WhoUnknown: ac.WhoUnknown, + CreatedAt: ac.CreatedAt, + ModifiedAt: ac.ModifiedAt, + } + + if ac.Category != nil { + details.Category = &DataAccessGrantCategory{ + ID: ac.Category.GrantCategory.Id, + Name: ac.Category.GrantCategory.Name, + NamePlural: ac.Category.GrantCategory.NamePlural, + IsSystem: ac.Category.GrantCategory.IsSystem, + IsDefault: ac.Category.GrantCategory.IsDefault, + } + } + + for _, sd := range ac.SyncData { + ds := sd.GetDataSource() + details.SyncData = append(details.SyncData, DataAccessSyncData{ + DataSourceID: ds.GetId(), + DataSourceName: ds.GetName(), + SyncStatus: string(sd.GetSyncStatus()), + }) + } + + return details +} diff --git a/pkg/tools/get_data_access_control_details.go b/pkg/tools/get_data_access_control_details.go new file mode 100644 index 0000000..a48b0f1 --- /dev/null +++ b/pkg/tools/get_data_access_control_details.go @@ -0,0 +1,46 @@ +package tools + +import ( + "context" + "fmt" + "net/http" + + "github.com/collibra/chip/pkg/chip" + "github.com/collibra/chip/pkg/clients" +) + +type DataAccessControlInput struct { + ID string `json:"id" jsonschema:"The id of the data access control to retrieve"` +} + +type DataAccessControlOutput struct { + AccessControl *clients.DataAccessControlDetails `json:"accessControl,omitempty" jsonschema:"The data access control details if found"` + Error string `json:"error,omitempty" jsonschema:"Error message if the access control could not be retrieved"` + Found bool `json:"found" jsonschema:"Whether the data access control was found"` +} + +func NewGetDataAccessControlDetailsTool(collibraClient *http.Client) *chip.Tool[DataAccessControlInput, DataAccessControlOutput] { + return &chip.Tool[DataAccessControlInput, DataAccessControlOutput]{ + Name: "get_data_access_control_details", + Description: "Retrieve detailed information about a specific Collibra Data Access control by its id. Returns the access control's name, description, state (ACTIVE, INACTIVE, DELETED), action type (GRANT, MASK, FILTER, SHARE, GROUP, FILTERRULE), grant category, policy rule, external management status, ABAC scope parse status, and timestamps. Use this to inspect an individual access control when you know its ID.", + Handler: handleGetDataAccessControlDetails(collibraClient), + Permissions: []string{}, + } +} + +func handleGetDataAccessControlDetails(collibraClient *http.Client) chip.ToolHandlerFunc[DataAccessControlInput, DataAccessControlOutput] { + return func(ctx context.Context, input DataAccessControlInput) (DataAccessControlOutput, error) { + details, err := clients.GetDataAccessControl(ctx, collibraClient, input.ID) + if err != nil { + return DataAccessControlOutput{ + Error: fmt.Sprintf("Failed to retrieve data access control: %s", err.Error()), + Found: false, + }, nil + } + + return DataAccessControlOutput{ + AccessControl: details, + Found: true, + }, nil + } +} diff --git a/pkg/tools/search_data_access_controls.go b/pkg/tools/search_data_access_controls.go new file mode 100644 index 0000000..53eca41 --- /dev/null +++ b/pkg/tools/search_data_access_controls.go @@ -0,0 +1,81 @@ +package tools + +import ( + "context" + "fmt" + "net/http" + + "github.com/collibra/chip/pkg/chip" + "github.com/collibra/chip/pkg/clients" +) + +type SearchDataAccessControlsInput struct { + Name string `json:"name,omitempty" jsonschema:"Optional. Filter by name (case-insensitive contains match)."` + Actions []string `json:"actions,omitempty" jsonschema:"Optional. Filter by one or more action types. Valid values: Grant, Mask, Filter, Share, Group, FilterRule."` + States []string `json:"states,omitempty" jsonschema:"Optional. Filter by one or more states. Valid values: Active, Inactive, Deleted."` + Cursor string `json:"cursor,omitempty" jsonschema:"Optional. Cursor from a previous response to fetch the next page of results."` + PageSize int `json:"pageSize,omitempty" jsonschema:"Optional. Number of results per page (default: 25, max: 25)."` +} + +type SearchDataAccessRolesInput struct { + Name string `json:"name,omitempty" jsonschema:"Optional. Filter by name (case-insensitive contains match)."` + States []string `json:"states,omitempty" jsonschema:"Optional. Filter by one or more states. Valid values: Active, Inactive, Deleted."` + Cursor string `json:"cursor,omitempty" jsonschema:"Optional. Cursor from a previous response to fetch the next page of results."` + PageSize int `json:"pageSize,omitempty" jsonschema:"Optional. Number of results per page (default: 25, max: 25)."` +} + +type SearchDataAccessControlsOutput struct { + Results []*clients.DataAccessControlDetails `json:"results" jsonschema:"The matching data access controls."` + NextCursor *string `json:"nextCursor,omitempty" jsonschema:"Cursor to pass in the next request to fetch the following page. Absent when there are no more results."` + Error string `json:"error,omitempty" jsonschema:"Error message if the search could not be completed."` +} + +func NewSearchDataAccessControlsTool(collibraClient *http.Client) *chip.Tool[SearchDataAccessControlsInput, SearchDataAccessControlsOutput] { + return &chip.Tool[SearchDataAccessControlsInput, SearchDataAccessControlsOutput]{ + Name: "search_data_access_controls", + Description: "Search for data access controls in Collibra Data Access. Results can be filtered by name (case-insensitive contains), action type (Grant, Mask, Filter, Share, Group, FilterRule), and/or state (Active, Inactive, Deleted). All filters are optional and can be combined. Returns a paginated list — use the returned cursor to fetch subsequent pages.", + Handler: handleSearchDataAccessControls(collibraClient), + Permissions: []string{}, + } +} + +func handleSearchDataAccessControls(collibraClient *http.Client) chip.ToolHandlerFunc[SearchDataAccessControlsInput, SearchDataAccessControlsOutput] { + return func(ctx context.Context, input SearchDataAccessControlsInput) (SearchDataAccessControlsOutput, error) { + result, err := clients.SearchDataAccessControls(ctx, collibraClient, input.Name, input.Actions, input.States, input.Cursor, input.PageSize) + if err != nil { + return SearchDataAccessControlsOutput{ + Error: fmt.Sprintf("Failed to search data access controls: %s", err.Error()), + }, nil + } + + return SearchDataAccessControlsOutput{ + Results: result.Items, + NextCursor: result.NextCursor, + }, nil + } +} + +func NewSearchDataAccessRolesTool(collibraClient *http.Client) *chip.Tool[SearchDataAccessRolesInput, SearchDataAccessControlsOutput] { + return &chip.Tool[SearchDataAccessRolesInput, SearchDataAccessControlsOutput]{ + Name: "search_data_access_roles", + Description: "Search for data access roles (Grant-type access controls) in Collibra Data Access. Results can be filtered by name (case-insensitive contains) and/or state (Active, Inactive, Deleted). All filters are optional and can be combined. Returns a paginated list — use the returned cursor to fetch subsequent pages.", + Handler: handleSearchDataAccessRoles(collibraClient), + Permissions: []string{}, + } +} + +func handleSearchDataAccessRoles(collibraClient *http.Client) chip.ToolHandlerFunc[SearchDataAccessRolesInput, SearchDataAccessControlsOutput] { + return func(ctx context.Context, input SearchDataAccessRolesInput) (SearchDataAccessControlsOutput, error) { + result, err := clients.SearchDataAccessControls(ctx, collibraClient, input.Name, []string{"Grant"}, input.States, input.Cursor, input.PageSize) + if err != nil { + return SearchDataAccessControlsOutput{ + Error: fmt.Sprintf("Failed to search data access roles: %s", err.Error()), + }, nil + } + + return SearchDataAccessControlsOutput{ + Results: result.Items, + NextCursor: result.NextCursor, + }, nil + } +} diff --git a/pkg/tools/search_data_access_identities.go b/pkg/tools/search_data_access_identities.go new file mode 100644 index 0000000..ad0fc64 --- /dev/null +++ b/pkg/tools/search_data_access_identities.go @@ -0,0 +1,48 @@ +package tools + +import ( + "context" + "fmt" + "net/http" + + "github.com/collibra/chip/pkg/chip" + "github.com/collibra/chip/pkg/clients" +) + +type SearchDataAccessIdentitiesInput struct { + Email string `json:"email,omitempty" jsonschema:"Optional. Exact email address to look up the user by."` + Name string `json:"name,omitempty" jsonschema:"Optional. Search string for a case-insensitive contains match on the user's display name. When used without email, SearchUsers is called server-side. When used with email, it is applied as a client-side filter on the result."` + Cursor string `json:"cursor,omitempty" jsonschema:"Optional. Cursor from a previous response to fetch the next page of results. Only applicable for name-based searches."` + PageSize int `json:"pageSize,omitempty" jsonschema:"Optional. Number of results per page (default: 25, max: 25). Only applicable for name-based searches."` +} + +type SearchDataAccessIdentitiesOutput struct { + Results []*clients.DataAccessIdentity `json:"results" jsonschema:"The matching Data Access users."` + NextCursor *string `json:"nextCursor,omitempty" jsonschema:"Cursor to pass in the next request to fetch the following page. Only present for name-based searches with more results available."` + Error string `json:"error,omitempty" jsonschema:"Error message if the search could not be completed."` +} + +func NewSearchDataAccessIdentitiesTool(collibraClient *http.Client) *chip.Tool[SearchDataAccessIdentitiesInput, SearchDataAccessIdentitiesOutput] { + return &chip.Tool[SearchDataAccessIdentitiesInput, SearchDataAccessIdentitiesOutput]{ + Name: "search_data_access_identities", + Description: "Search for Data Access users (identities) by name and/or email. Providing email performs an exact lookup; providing name performs a case-insensitive contains search via SearchUsers. Both can be combined: email resolves the user, name filters the result client-side. Name-only searches are paginated — use the returned cursor to fetch subsequent pages.", + Handler: handleSearchDataAccessIdentities(collibraClient), + Permissions: []string{}, + } +} + +func handleSearchDataAccessIdentities(collibraClient *http.Client) chip.ToolHandlerFunc[SearchDataAccessIdentitiesInput, SearchDataAccessIdentitiesOutput] { + return func(ctx context.Context, input SearchDataAccessIdentitiesInput) (SearchDataAccessIdentitiesOutput, error) { + result, err := clients.SearchDataAccessIdentities(ctx, collibraClient, input.Name, input.Email, input.Cursor, input.PageSize) + if err != nil { + return SearchDataAccessIdentitiesOutput{ + Error: fmt.Sprintf("Failed to search Data Access identities: %s", err.Error()), + }, nil + } + + return SearchDataAccessIdentitiesOutput{ + Results: result.Items, + NextCursor: result.NextCursor, + }, nil + } +} diff --git a/pkg/tools/tools_register.go b/pkg/tools/tools_register.go index d84951c..f14a943 100644 --- a/pkg/tools/tools_register.go +++ b/pkg/tools/tools_register.go @@ -29,6 +29,10 @@ func RegisterAll(server *chip.Server, client *http.Client, toolConfig *chip.Serv toolRegister(server, toolConfig, NewSearchLineageEntitiesTool(client)) toolRegister(server, toolConfig, NewGetLineageTransformationTool(client)) toolRegister(server, toolConfig, NewSearchLineageTransformationsTool(client)) + toolRegister(server, toolConfig, NewGetDataAccessControlDetailsTool(client)) + toolRegister(server, toolConfig, NewSearchDataAccessControlsTool(client)) + toolRegister(server, toolConfig, NewSearchDataAccessRolesTool(client)) + toolRegister(server, toolConfig, NewSearchDataAccessIdentitiesTool(client)) } func toolRegister[In, Out any](server *chip.Server, toolConfig *chip.ServerToolConfig, tool *chip.Tool[In, Out]) { From 57e7aec953756174363e477a10fb8e4e42df882e Mon Sep 17 00:00:00 2001 From: Wouter Cordewiner <73988312+wouterc-collibra@users.noreply.github.com> Date: Fri, 15 May 2026 10:48:19 +0200 Subject: [PATCH 02/13] Tools for Data Access --- SKILLS.md | 25 +- go.mod | 12 +- go.sum | 37 ++- pkg/clients/data_access_client.go | 264 +++++++++++++----- pkg/tools/create_data_access_request/tool.go | 131 +++++++++ .../tool.go} | 6 +- pkg/tools/register.go | 12 +- pkg/tools/search_data_access_controls.go | 81 ------ .../tool.go} | 23 +- pkg/tools/search_data_access_objects/tool.go | 51 ++++ 10 files changed, 454 insertions(+), 188 deletions(-) create mode 100644 pkg/tools/create_data_access_request/tool.go rename pkg/tools/{get_data_access_control_details.go => get_data_access_control_details/tool.go} (87%) delete mode 100644 pkg/tools/search_data_access_controls.go rename pkg/tools/{search_data_access_identities.go => search_data_access_identities/tool.go} (50%) create mode 100644 pkg/tools/search_data_access_objects/tool.go diff --git a/SKILLS.md b/SKILLS.md index 5e1108c..2261928 100644 --- a/SKILLS.md +++ b/SKILLS.md @@ -87,13 +87,17 @@ These tools query the technical lineage graph — a map of all data objects and These tools query Collibra Data Access — the system that manages who can access what data, through grants, masks, filters, and groups. -**`search_data_access_controls`** — Search for data access controls. All filters are optional and combinable: `name` (case-insensitive contains), `actions` (one or more of `Grant`, `Mask`, `Filter`, `Share`, `Group`, `FilterRule`), `states` (one or more of `Active`, `Inactive`, `Deleted`). Returns a paginated list (25 per page); pass the returned `nextCursor` to fetch subsequent pages. +**`search_data_access_identities`** — Search for Data Access users (identities) by name and/or email. Providing `email` performs an exact lookup via `GetUserByEmail`. Providing `name` performs a server-side case-insensitive contains search via `SearchUsers`. Both can be combined: email resolves the user, name filters the result client-side. Name-only searches are paginated (25 per page) — use the returned `nextCursor` to fetch subsequent pages. -**`search_data_access_roles`** — Alias of `search_data_access_controls` restricted to `Grant`-type controls. Use this when the user asks specifically about roles or who has been granted access. Supports the same `name` and `states` filters; the `actions` filter is fixed to `Grant` and cannot be overridden. Returns a paginated list (25 per page); pass the returned `nextCursor` to fetch subsequent pages. +**`search_data_access_objects`** — Search for data objects in Collibra Data Access (tables, columns, schemas, views, and other entities tracked in registered data sources). Filters can be combined: `name` (case-insensitive contains), `dataSources` (data source IDs), `types` (e.g. `table`, `column`, `schema`, `view`), `parents` / `ancestors` (other data object IDs to scope the search to a sub-tree), and `includeDeleted`. Returns up to `pageSize` matches (default 25, max 25). Each result includes the data object ID, name, fully qualified name, type, data type, deleted flag, description, data source ID, and `applicablePermissions` — the list of source-system permissions (each with a `name` and `description`) that can be requested on the object. Use those names when populating `what[].permissions` for `create_data_access_request`. -**`get_data_access_control_details`** — Retrieve full details for a single data access control by its id. Use this when you already have an access control ID and need to inspect it. +**`create_data_access_request`** — Create a new Collibra Data Access request on behalf of one or more users for one or more data objects. Destructive. Required behavior: -**`search_data_access_identities`** — Search for Data Access users (identities) by name and/or email. Providing `email` performs an exact lookup via `GetUserByEmail`. Providing `name` performs a server-side case-insensitive contains search via `SearchUsers`. Both can be combined: email resolves the user, name filters the result client-side. Name-only searches are paginated (25 per page) — use the returned `nextCursor` to fetch subsequent pages. +- **Minimum input is WHO, WHAT, and a purpose.** Do not call this tool until all three are supplied. +- **WHO** must be resolved via `search_data_access_identities` (by email or name) — pass the returned user IDs in `userIds`. Never pass raw emails or names. +- **WHAT** must be resolved via `search_data_access_objects` — pass the returned data object IDs in `what[].dataObjectId`. Per item, `permissions` should be empty and `globalPermissions` must always be READ. +- **Purpose** is mandatory and must come from the user — it is the business justification for the request. If the user has not stated a purpose, ask them for one before calling the tool. Do not invent a purpose. The tool always appends a note stating that the request was created by AI. +- **Name** is optional. If the user does not provide one, omit `name` on the first call. The tool will return status `needs_name_confirmation` with a `suggestedName` derived from the purpose — present that suggestion to the user, get their confirmation (or an alternative), and call again with the confirmed value in `name`. ### Data Contracts @@ -147,13 +151,8 @@ These tools query Collibra Data Access — the system that manages who can acces 2. `get_lineage_downstream` → relations with consumer entity IDs 3. Summarize based on the graph structure — only call `get_lineage_entity` for the most relevant consumers, not all of them -### Find and inspect data access controls -1. `search_data_access_controls` with optional name/action/state filters → get matching controls and their IDs -2. `get_data_access_control_details` with a specific ID → full details including grant category, policy rule, timestamps - ### Find and inspect data access roles -1. `search_data_access_roles` with optional name/state filters → get matching controls and their IDs -2. `get_data_access_control_details` with a specific ID → full details including grant category, policy rule, timestamps +1. `get_data_access_control_details` with a specific ID → full details including grant category, policy rule, timestamps ### Find who has been granted access (roles) 1. `search_data_access_roles` with optional name/state filters → returns only Grant-type controls @@ -163,6 +162,12 @@ These tools query Collibra Data Access — the system that manages who can acces 1. `search_data_access_identities` with `email` → exact lookup, returns the user's id, display name, and type — or with `name` → paginated server-side contains search across all users +### Create a Data Access request +1. Make sure the user has stated a `purpose` — the business justification for the request. If missing, ask for it before continuing. +2. `search_data_access_identities` for every beneficiary → collect the user IDs (the WHO) +3. `search_data_access_objects` for every data object the users need → collect the data object IDs (the WHAT) +4. `create_data_access_request` with `purpose`, `userIds`, and `what` — if the user has not provided a name, omit `name`. The tool returns `needs_name_confirmation` with a `suggestedName` derived from the purpose; confirm it with the user, then call again with `name` set. The purpose is used as the description, with an AI-created note appended automatically. + ### Manage a data contract 1. `list_data_contract` to find the contract UUID 2. `pull_data_contract_manifest` to download, edit, then `push_data_contract_manifest` to update diff --git a/go.mod b/go.mod index 75e64f0..e2e5ce4 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,9 @@ module github.com/collibra/chip -go 1.25.0 +go 1.26.2 require ( + github.com/collibra/data-access-go-sdk v1.0.0 github.com/google/go-querystring v1.1.0 github.com/google/jsonschema-go v0.4.2 github.com/google/uuid v1.6.0 @@ -11,9 +12,15 @@ require ( github.com/spf13/viper v1.21.0 ) +replace github.com/collibra/data-access-go-sdk => /Users/wouterc/w/data-access-go-sdk-mcp + require ( + github.com/Khan/genqlient v0.8.2-0.20251028054717-8ddeeee0a15a // indirect + github.com/Masterminds/semver/v3 v3.5.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-retryablehttp v0.7.8 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/sagikazarmark/locafero v0.12.0 // indirect @@ -22,10 +29,11 @@ require ( github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/vektah/gqlparser/v2 v2.5.30 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/oauth2 v0.35.0 // indirect golang.org/x/sys v0.41.0 // indirect golang.org/x/text v0.32.0 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index c47ee63..7d659b8 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,13 @@ -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/Khan/genqlient v0.8.2-0.20251028054717-8ddeeee0a15a h1:kx/iDWW6lRgNBqTL1z6UB7ajcZR3OcVLKVJ39QkUnUw= +github.com/Khan/genqlient v0.8.2-0.20251028054717-8ddeeee0a15a/go.mod h1:guUHwMi8ByjIvs3TyAPM+V9ryaW305CtK7+aCeP2Jzc= +github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= +github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= @@ -17,19 +25,26 @@ github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbc github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= +github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/modelcontextprotocol/go-sdk v1.5.0 h1:CHU0FIX9kpueNkxuYtfYQn1Z0slhFzBZuq+x6IiblIU= github.com/modelcontextprotocol/go-sdk v1.5.0/go.mod h1:gggDIhoemhWs3BGkGwd1umzEXCEMMvAnhTrnbXJKKKA= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= @@ -38,6 +53,8 @@ github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc= github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg= github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0= github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0= +github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= @@ -50,6 +67,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/vektah/gqlparser/v2 v2.5.30 h1:EqLwGAFLIzt1wpx1IPpY67DwUujF1OfzgEyDsLrN6kE= +github.com/vektah/gqlparser/v2 v2.5.30/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= @@ -60,8 +79,8 @@ golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= -golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= -golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/pkg/clients/data_access_client.go b/pkg/clients/data_access_client.go index 4bff19a..1cb02a8 100644 --- a/pkg/clients/data_access_client.go +++ b/pkg/clients/data_access_client.go @@ -123,56 +123,6 @@ type SearchDataAccessControlsResult struct { NextCursor *string `json:"nextCursor,omitempty"` } -// SearchDataAccessControls returns a page of data access controls filtered by name, actions, and/or states. -// Name search is case-insensitive contains. Pass cursor from a previous response to fetch the next page. -func SearchDataAccessControls(ctx context.Context, httpClient *http.Client, name string, actions []string, states []string, cursor string, pageSize int) (*SearchDataAccessControlsResult, error) { - collibraHost, ok := chip.GetCollibraHost(ctx) - if !ok { - return nil, fmt.Errorf("collibra host not configured in context") - } - dataAccessURL := strings.TrimSuffix(collibraHost, "/") + "/dataAccess" - - collibraClient, err := sdk.NewClient(dataAccessURL, sdk.WithHTTPClient(httpClient)) - if err != nil { - return nil, fmt.Errorf("failed to create data access client: %w", err) - } - - filter := &types.AccessControlFilterInput{} - if name != "" { - filter.Search = &name - } - for _, a := range actions { - filter.Actions = append(filter.Actions, types.AccessControlAction(a)) - } - for _, s := range states { - filter.States = append(filter.States, types.AccessControlState(s)) - } - - opts := []func(*services.AccessControlListOptions){ - services.WithAccessControlListFilter(filter), - } - if cursor != "" { - opts = append(opts, services.WithAccessControlListCursor(cursor)) - } - if pageSize > 0 { - opts = append(opts, services.WithAccessControlListPageSize(pageSize)) - } - - items, nextCursor, err := collibraClient.AccessControl().ListAccessControlsPage(ctx, opts...) - if err != nil { - return nil, err - } - - result := &SearchDataAccessControlsResult{ - Items: make([]*DataAccessControlDetails, 0, len(items)), - NextCursor: nextCursor, - } - for _, ac := range items { - result.Items = append(result.Items, mapToDataAccessControlDetails(ac)) - } - return result, nil -} - func mapToDataAccessWhatItem(w *types.AccessWhatAccessControlItem) DataAccessWhatItem { item := DataAccessWhatItem{ ExpiresAt: w.ExpiresAt, @@ -227,18 +177,17 @@ type DataAccessIdentity struct { Type string `json:"type" jsonschema:"User type: Human or Machine"` } -// SearchDataAccessIdentitiesResult holds a page of identities and an optional next-page cursor. +// SearchDataAccessIdentitiesResult holds a page of identities. type SearchDataAccessIdentitiesResult struct { - Items []*DataAccessIdentity - NextCursor *string + Items []*DataAccessIdentity } // SearchDataAccessIdentities searches for Data Access users by name and/or email. // When email is provided, an exact lookup via GetUserByEmail is performed. Name is then applied // as an optional client-side case-insensitive contains filter on the result. -// When only name is provided, SearchUsers is called with the Search filter (case-insensitive -// contains). Cursor and pageSize control pagination for name-based searches. -func SearchDataAccessIdentities(ctx context.Context, httpClient *http.Client, name, email, cursor string, pageSize int) (*SearchDataAccessIdentitiesResult, error) { +// When only name is provided, ListUsers is called with the Search filter (case-insensitive +// contains). The returned list is capped at pageSize items (default 25). +func SearchDataAccessIdentities(ctx context.Context, httpClient *http.Client, name, email string, pageSize int) (*SearchDataAccessIdentitiesResult, error) { collibraHost, ok := chip.GetCollibraHost(ctx) if !ok { return nil, fmt.Errorf("collibra host not configured in context") @@ -267,36 +216,211 @@ func SearchDataAccessIdentities(ctx context.Context, httpClient *http.Client, na return &SearchDataAccessIdentitiesResult{Items: []*DataAccessIdentity{identity}}, nil } - // Name-only path: use SearchUsers with the Search filter. filter := &types.UserFilterInput{} if name != "" { filter.Search = &name } - var after *string - if cursor != "" { - after = &cursor + limit := pageSize + if limit <= 0 { + limit = 25 } - var limit *int - if pageSize > 0 { - limit = &pageSize + + result := &SearchDataAccessIdentitiesResult{ + Items: make([]*DataAccessIdentity, 0, limit), } + for user, iterErr := range collibraClient.User().ListUsers(ctx, services.WithUserListFilter(filter)) { + if iterErr != nil { + return nil, iterErr + } + result.Items = append(result.Items, mapToDataAccessIdentity(user)) + if len(result.Items) >= limit { + break + } + } + return result, nil +} - users, nextCursor, err := collibraClient.User().SearchUsers(ctx, after, limit, filter) +// DataAccessObject represents a single data object in Collibra Data Access. +type DataAccessObject struct { + ID string `json:"id" jsonschema:"Unique identifier of the data object"` + Name string `json:"name" jsonschema:"Name of the data object"` + FullName string `json:"fullName" jsonschema:"Fully qualified name of the data object within its data source"` + Type string `json:"type" jsonschema:"Type of the data object (e.g. table, column, schema, view)"` + DataType *string `json:"dataType,omitempty" jsonschema:"Data type of the object (typically used for columns)"` + Deleted bool `json:"deleted" jsonschema:"Whether the data object is deleted (no longer present in the source)"` + Description string `json:"description" jsonschema:"Description of the data object"` + DataSourceID string `json:"dataSourceId,omitempty" jsonschema:"Identifier of the data source the object belongs to"` + ApplicablePermissions []DataAccessPermission `json:"applicablePermissions,omitempty" jsonschema:"Source-system permissions that can be requested or granted on this data object (and its descendants). Each permission carries its name and description."` +} + +// DataAccessPermission is a permission that can be set on a data object. +type DataAccessPermission struct { + Name string `json:"name" jsonschema:"Permission name as defined by the data source (e.g. SELECT, INSERT)"` + Description string `json:"description" jsonschema:"Human-readable description of the permission"` +} + +// SearchDataAccessObjectsResult holds a page of data objects. +type SearchDataAccessObjectsResult struct { + Items []*DataAccessObject `json:"items"` +} + +// SearchDataAccessObjects returns a list of data objects matching the supplied filters. +// Name search is case-insensitive contains. The returned list is capped at pageSize items +// (default 25), drawn from the SDK's ListDataObjects iterator. +func SearchDataAccessObjects(ctx context.Context, httpClient *http.Client, name string, dataSources []string, dataObjectTypes []string, parents []string, ancestors []string, includeDeleted bool, pageSize int) (*SearchDataAccessObjectsResult, error) { + collibraHost, ok := chip.GetCollibraHost(ctx) + if !ok { + return nil, fmt.Errorf("collibra host not configured in context") + } + dataAccessURL := strings.TrimSuffix(collibraHost, "/") + "/dataAccess" + + collibraClient, err := sdk.NewClient(dataAccessURL, sdk.WithHTTPClient(httpClient)) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to create data access client: %w", err) } - result := &SearchDataAccessIdentitiesResult{ - Items: make([]*DataAccessIdentity, 0, len(users)), - NextCursor: nextCursor, + filter := &types.DataObjectFilterInput{} + if name != "" { + filter.Search = &name + } + if len(dataSources) > 0 { + filter.DataSources = dataSources + } + if len(dataObjectTypes) > 0 { + filter.Types = dataObjectTypes } - for i := range users { - result.Items = append(result.Items, mapToDataAccessIdentity(&users[i])) + if len(parents) > 0 { + filter.Parents = parents + } + if len(ancestors) > 0 { + filter.Ancestors = ancestors + } + if includeDeleted { + filter.IncludeDeleted = &includeDeleted + } + + limit := pageSize + if limit <= 0 { + limit = 25 + } + + result := &SearchDataAccessObjectsResult{ + Items: make([]*DataAccessObject, 0, limit), + } + for obj, iterErr := range collibraClient.DataObject().ListDataObjects(ctx, services.WithDataObjectListFilter(filter)) { + if iterErr != nil { + return nil, iterErr + } + result.Items = append(result.Items, mapToDataAccessObject(obj)) + if len(result.Items) >= limit { + break + } } return result, nil } +// CreateDataAccessRequestWhatInput describes a single WHAT item (a data object) for a new +// data access request, with optional requested permissions. +type CreateDataAccessRequestWhatInput struct { + DataObjectID string `json:"dataObjectId" jsonschema:"The ID of the data object the requesters want access to. Obtain via search_data_access_objects."` + Permissions []string `json:"permissions,omitempty" jsonschema:"Source-system permissions requested on this data object (e.g. SELECT). Should always be empty."` + GlobalPermissions []string `json:"globalPermissions,omitempty" jsonschema:"Global permissions requested on this data object. Must always be READ."` +} + +// CreateDataAccessRequestInput holds the parameters required to create a new data access request. +type CreateDataAccessRequestInput struct { + Name *string + Description string + UserIDs []string + What []CreateDataAccessRequestWhatInput +} + +// DataAccessRequestSummary is the simplified result of creating an access request. +type DataAccessRequestSummary struct { + ID string `json:"id" jsonschema:"Unique identifier of the created access request"` + Name *string `json:"name,omitempty" jsonschema:"Display name of the access request"` + Description string `json:"description" jsonschema:"Description of the access request"` + Status string `json:"status" jsonschema:"Current status of the access request (e.g. Created, Approval, Implementation, Closed)"` + Outcome string `json:"outcome" jsonschema:"Current outcome of the access request"` + Url string `json:"url" jsonschema:"Url in the Collibra UI to view access request"` +} + +// CreateDataAccessRequest creates a new Data Access request via the SDK's AccessRequestClient. +func CreateDataAccessRequest(ctx context.Context, httpClient *http.Client, input CreateDataAccessRequestInput) (*DataAccessRequestSummary, error) { + collibraHost, ok := chip.GetCollibraHost(ctx) + if !ok { + return nil, fmt.Errorf("collibra host not configured in context") + } + dataAccessURL := strings.TrimSuffix(collibraHost, "/") + "/dataAccess" + + collibraClient, err := sdk.NewClient(dataAccessURL, sdk.WithHTTPClient(httpClient)) + if err != nil { + return nil, fmt.Errorf("failed to create data access client: %w", err) + } + + what := make([]types.AccessRequestWhatInput, 0, len(input.What)) + for _, w := range input.What { + what = append(what, types.AccessRequestWhatInput{ + DataObject: &types.AccessRequestDataObjectWhatInput{ + Id: w.DataObjectID, + Permissions: w.Permissions, + GlobalPermissions: w.GlobalPermissions, + }, + }) + } + + req := types.AccessRequestInput{ + Name: input.Name, + Description: &input.Description, + Who: &types.AccessRequestWhoInput{ + Users: input.UserIDs, + }, + What: what, + } + + ar, err := collibraClient.AccessRequest().CreateAccessRequest(ctx, req) + if err != nil { + return nil, err + } + + requestURL := strings.TrimSuffix(collibraHost, "/") + "/data-access/access-requests/" + ar.Id + + return &DataAccessRequestSummary{ + ID: ar.Id, + Name: ar.Name, + Description: ar.Description, + Status: string(ar.Status), + Outcome: string(ar.Outcome), + Url: requestURL, + }, nil +} + +func mapToDataAccessObject(o *types.DataObject) *DataAccessObject { + out := &DataAccessObject{ + ID: o.Id, + Name: o.Name, + FullName: o.FullName, + Type: o.Type, + DataType: o.DataType, + Deleted: o.Deleted, + Description: o.Description, + } + if o.DataSource != nil { + out.DataSourceID = o.DataSource.Id + } + if len(o.ApplicablePermissions) > 0 { + out.ApplicablePermissions = make([]DataAccessPermission, 0, len(o.ApplicablePermissions)) + for _, p := range o.ApplicablePermissions { + out.ApplicablePermissions = append(out.ApplicablePermissions, DataAccessPermission{ + Name: p.Name, + Description: p.Description, + }) + } + } + return out +} + func mapToDataAccessIdentity(u *types.User) *DataAccessIdentity { return &DataAccessIdentity{ ID: u.Id, diff --git a/pkg/tools/create_data_access_request/tool.go b/pkg/tools/create_data_access_request/tool.go new file mode 100644 index 0000000..b96ba08 --- /dev/null +++ b/pkg/tools/create_data_access_request/tool.go @@ -0,0 +1,131 @@ +package create_data_access_request + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/collibra/chip/pkg/chip" + "github.com/collibra/chip/pkg/clients" + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +// aiDescriptionSuffix is appended to every description so the access request is clearly +// attributed to an AI agent. +const aiDescriptionSuffix = "This access request was created by AI." + +// suggestedNameMaxLen caps the length of a name suggestion derived from the purpose. +const suggestedNameMaxLen = 80 + +// Status values returned in the Output. +const ( + statusNeedsNameConfirmation = "needs_name_confirmation" + statusCreated = "created" +) + +type Input struct { + Name string `json:"name,omitempty" jsonschema:"Optional. Display name of the access request. If omitted, the tool returns a suggested name derived from the purpose and asks the agent to confirm it with the user before retrying."` + Purpose string `json:"purpose" jsonschema:"Required. The user-supplied purpose / business justification for the access request. This is used verbatim as the description of the access request. The tool always appends a note indicating the request was created by AI."` + UserIDs []string `json:"userIds" jsonschema:"Required. IDs of the beneficiary users (the WHO of the request). Resolve these via the search_data_access_identities tool before calling."` + What []clients.CreateDataAccessRequestWhatInput `json:"what" jsonschema:"Required. The data objects the users are requesting access to (the WHAT of the request). Each item references a data object ID and optional requested permissions. Resolve the data object IDs via the search_data_access_objects tool before calling."` +} + +type Output struct { + Status string `json:"status,omitempty" jsonschema:"Outcome of the call: needs_name_confirmation (no name was supplied — confirm the suggestedName with the user and call again with name set), or created (the request was successfully created)."` + Message string `json:"message,omitempty" jsonschema:"Human-readable explanation of the status. When status is needs_name_confirmation, this tells the agent to confirm the suggested name with the user."` + SuggestedName string `json:"suggestedName,omitempty" jsonschema:"Name suggestion derived from the purpose. Present only when status is needs_name_confirmation."` + Request *clients.DataAccessRequestSummary `json:"request,omitempty" jsonschema:"The created access request, if successful."` + Error string `json:"error,omitempty" jsonschema:"Error message if the access request could not be created."` +} + +func NewTool(collibraClient *http.Client) *chip.Tool[Input, Output] { + return &chip.Tool[Input, Output]{ + Name: "create_data_access_request", + Description: "Create a new Collibra Data Access request. Requires the WHO (beneficiary user IDs, obtained via search_data_access_identities), the WHAT (data objects, obtained via search_data_access_objects), and a user-supplied purpose that is used as the description. If no name is supplied, the tool returns a suggested name derived from the purpose with status needs_name_confirmation — confirm the suggestion (or get a replacement) with the user, then call again with name set. The description always ends with a note stating that the request was created by AI.", + Handler: handle(collibraClient), + Permissions: []string{}, + Annotations: &mcp.ToolAnnotations{ReadOnlyHint: false, DestructiveHint: new(false)}, + } +} + +func handle(collibraClient *http.Client) chip.ToolHandlerFunc[Input, Output] { + return func(ctx context.Context, input Input) (Output, error) { + purpose := strings.TrimSpace(input.Purpose) + if purpose == "" { + return Output{Error: "purpose is required — ask the user for the business justification for this access request"}, nil + } + if len(input.UserIDs) == 0 { + return Output{Error: "at least one beneficiary user ID is required — resolve them with search_data_access_identities"}, nil + } + if len(input.What) == 0 { + return Output{Error: "at least one data object is required — resolve them with search_data_access_objects"}, nil + } + for i, w := range input.What { + if strings.TrimSpace(w.DataObjectID) == "" { + return Output{Error: fmt.Sprintf("what[%d].dataObjectId is required", i)}, nil + } + } + + name := strings.TrimSpace(input.Name) + if name == "" { + suggested := suggestNameFromPurpose(purpose) + return Output{ + Status: statusNeedsNameConfirmation, + SuggestedName: suggested, + Message: fmt.Sprintf("No name was supplied. Suggested name based on the purpose: %q. Confirm this with the user (or ask for a different name), then call create_data_access_request again with the confirmed name in the `name` field.", suggested), + }, nil + } + + clientInput := clients.CreateDataAccessRequestInput{ + Name: &name, + Description: buildDescription(purpose), + UserIDs: input.UserIDs, + What: input.What, + } + + req, err := clients.CreateDataAccessRequest(ctx, collibraClient, clientInput) + if err != nil { + return Output{Error: fmt.Sprintf("Failed to create data access request: %s", err.Error())}, nil + } + return Output{Status: statusCreated, Request: req}, nil + } +} + +func buildDescription(purpose string) string { + if strings.Contains(purpose, aiDescriptionSuffix) { + return purpose + } + if !strings.HasSuffix(purpose, ".") { + purpose = purpose + "." + } + return purpose + " " + aiDescriptionSuffix +} + +// suggestNameFromPurpose derives a short, human-readable name from the purpose text. +// It takes the first sentence/line, strips the AI-attribution suffix, collapses whitespace, +// truncates to suggestedNameMaxLen characters at a word boundary, and prefixes it. +func suggestNameFromPurpose(purpose string) string { + summary := strings.ReplaceAll(purpose, aiDescriptionSuffix, "") + summary = strings.Map(func(r rune) rune { + if r == '\n' || r == '\r' || r == '\t' { + return ' ' + } + return r + }, summary) + if idx := strings.IndexAny(summary, ".!?"); idx >= 0 { + summary = summary[:idx] + } + summary = strings.Join(strings.Fields(summary), " ") + if summary == "" { + return "Access request" + } + if len(summary) > suggestedNameMaxLen { + truncated := summary[:suggestedNameMaxLen] + if sp := strings.LastIndex(truncated, " "); sp > suggestedNameMaxLen/2 { + truncated = truncated[:sp] + } + summary = strings.TrimRight(truncated, " ,;:-") + } + return "Access request: " + summary +} diff --git a/pkg/tools/get_data_access_control_details.go b/pkg/tools/get_data_access_control_details/tool.go similarity index 87% rename from pkg/tools/get_data_access_control_details.go rename to pkg/tools/get_data_access_control_details/tool.go index a48b0f1..328d018 100644 --- a/pkg/tools/get_data_access_control_details.go +++ b/pkg/tools/get_data_access_control_details/tool.go @@ -1,4 +1,4 @@ -package tools +package get_data_access_control_details import ( "context" @@ -7,6 +7,7 @@ import ( "github.com/collibra/chip/pkg/chip" "github.com/collibra/chip/pkg/clients" + "github.com/modelcontextprotocol/go-sdk/mcp" ) type DataAccessControlInput struct { @@ -19,12 +20,13 @@ type DataAccessControlOutput struct { Found bool `json:"found" jsonschema:"Whether the data access control was found"` } -func NewGetDataAccessControlDetailsTool(collibraClient *http.Client) *chip.Tool[DataAccessControlInput, DataAccessControlOutput] { +func NewTool(collibraClient *http.Client) *chip.Tool[DataAccessControlInput, DataAccessControlOutput] { return &chip.Tool[DataAccessControlInput, DataAccessControlOutput]{ Name: "get_data_access_control_details", Description: "Retrieve detailed information about a specific Collibra Data Access control by its id. Returns the access control's name, description, state (ACTIVE, INACTIVE, DELETED), action type (GRANT, MASK, FILTER, SHARE, GROUP, FILTERRULE), grant category, policy rule, external management status, ABAC scope parse status, and timestamps. Use this to inspect an individual access control when you know its ID.", Handler: handleGetDataAccessControlDetails(collibraClient), Permissions: []string{}, + Annotations: &mcp.ToolAnnotations{ReadOnlyHint: true, DestructiveHint: new(false)}, } } diff --git a/pkg/tools/register.go b/pkg/tools/register.go index 6845198..126764b 100644 --- a/pkg/tools/register.go +++ b/pkg/tools/register.go @@ -7,11 +7,13 @@ import ( "github.com/collibra/chip/pkg/tools/add_business_term" "github.com/collibra/chip/pkg/tools/add_data_classification_match" "github.com/collibra/chip/pkg/tools/create_asset" + "github.com/collibra/chip/pkg/tools/create_data_access_request" "github.com/collibra/chip/pkg/tools/discover_business_glossary" "github.com/collibra/chip/pkg/tools/discover_data_assets" "github.com/collibra/chip/pkg/tools/get_asset_details" "github.com/collibra/chip/pkg/tools/get_business_term_data" "github.com/collibra/chip/pkg/tools/get_column_semantics" + "github.com/collibra/chip/pkg/tools/get_data_access_control_details" "github.com/collibra/chip/pkg/tools/get_lineage_downstream" "github.com/collibra/chip/pkg/tools/get_lineage_entity" "github.com/collibra/chip/pkg/tools/get_lineage_transformation" @@ -20,14 +22,16 @@ import ( "github.com/collibra/chip/pkg/tools/get_table_semantics" "github.com/collibra/chip/pkg/tools/list_asset_types" "github.com/collibra/chip/pkg/tools/list_data_contracts" - "github.com/collibra/chip/pkg/tools/prepare_create_asset" "github.com/collibra/chip/pkg/tools/prepare_add_business_term" + "github.com/collibra/chip/pkg/tools/prepare_create_asset" "github.com/collibra/chip/pkg/tools/pull_data_contract_manifest" "github.com/collibra/chip/pkg/tools/push_data_contract_manifest" "github.com/collibra/chip/pkg/tools/remove_data_classification_match" "github.com/collibra/chip/pkg/tools/search_asset_keyword" - "github.com/collibra/chip/pkg/tools/search_data_classification_matches" + "github.com/collibra/chip/pkg/tools/search_data_access_identities" + "github.com/collibra/chip/pkg/tools/search_data_access_objects" "github.com/collibra/chip/pkg/tools/search_data_classes" + "github.com/collibra/chip/pkg/tools/search_data_classification_matches" "github.com/collibra/chip/pkg/tools/search_lineage_entities" "github.com/collibra/chip/pkg/tools/search_lineage_transformations" ) @@ -56,17 +60,21 @@ func RegisterAll(server *chip.Server, client *http.Client, toolConfig *chip.Serv toolRegister(server, toolConfig, prepare_add_business_term.NewTool(client)) toolRegister(server, toolConfig, get_business_term_data.NewTool(client)) toolRegister(server, toolConfig, get_column_semantics.NewTool(client)) + toolRegister(server, toolConfig, get_data_access_control_details.NewTool(client)) toolRegister(server, toolConfig, get_lineage_downstream.NewTool(client)) toolRegister(server, toolConfig, get_lineage_entity.NewTool(client)) toolRegister(server, toolConfig, get_lineage_transformation.NewTool(client)) toolRegister(server, toolConfig, get_lineage_upstream.NewTool(client)) toolRegister(server, toolConfig, get_measure_data.NewTool(client)) toolRegister(server, toolConfig, get_table_semantics.NewTool(client)) + toolRegister(server, toolConfig, search_data_access_identities.NewTool(client)) + toolRegister(server, toolConfig, search_data_access_objects.NewTool(client)) toolRegister(server, toolConfig, search_lineage_entities.NewTool(client)) toolRegister(server, toolConfig, search_lineage_transformations.NewTool(client)) toolRegister(server, toolConfig, prepare_create_asset.NewTool(client)) toolRegister(server, toolConfig, add_business_term.NewTool(client)) toolRegister(server, toolConfig, create_asset.NewTool(client)) + toolRegister(server, toolConfig, create_data_access_request.NewTool(client)) } func toolRegister[In, Out any](server *chip.Server, toolConfig *chip.ServerToolConfig, tool *chip.Tool[In, Out]) { diff --git a/pkg/tools/search_data_access_controls.go b/pkg/tools/search_data_access_controls.go deleted file mode 100644 index 53eca41..0000000 --- a/pkg/tools/search_data_access_controls.go +++ /dev/null @@ -1,81 +0,0 @@ -package tools - -import ( - "context" - "fmt" - "net/http" - - "github.com/collibra/chip/pkg/chip" - "github.com/collibra/chip/pkg/clients" -) - -type SearchDataAccessControlsInput struct { - Name string `json:"name,omitempty" jsonschema:"Optional. Filter by name (case-insensitive contains match)."` - Actions []string `json:"actions,omitempty" jsonschema:"Optional. Filter by one or more action types. Valid values: Grant, Mask, Filter, Share, Group, FilterRule."` - States []string `json:"states,omitempty" jsonschema:"Optional. Filter by one or more states. Valid values: Active, Inactive, Deleted."` - Cursor string `json:"cursor,omitempty" jsonschema:"Optional. Cursor from a previous response to fetch the next page of results."` - PageSize int `json:"pageSize,omitempty" jsonschema:"Optional. Number of results per page (default: 25, max: 25)."` -} - -type SearchDataAccessRolesInput struct { - Name string `json:"name,omitempty" jsonschema:"Optional. Filter by name (case-insensitive contains match)."` - States []string `json:"states,omitempty" jsonschema:"Optional. Filter by one or more states. Valid values: Active, Inactive, Deleted."` - Cursor string `json:"cursor,omitempty" jsonschema:"Optional. Cursor from a previous response to fetch the next page of results."` - PageSize int `json:"pageSize,omitempty" jsonschema:"Optional. Number of results per page (default: 25, max: 25)."` -} - -type SearchDataAccessControlsOutput struct { - Results []*clients.DataAccessControlDetails `json:"results" jsonschema:"The matching data access controls."` - NextCursor *string `json:"nextCursor,omitempty" jsonschema:"Cursor to pass in the next request to fetch the following page. Absent when there are no more results."` - Error string `json:"error,omitempty" jsonschema:"Error message if the search could not be completed."` -} - -func NewSearchDataAccessControlsTool(collibraClient *http.Client) *chip.Tool[SearchDataAccessControlsInput, SearchDataAccessControlsOutput] { - return &chip.Tool[SearchDataAccessControlsInput, SearchDataAccessControlsOutput]{ - Name: "search_data_access_controls", - Description: "Search for data access controls in Collibra Data Access. Results can be filtered by name (case-insensitive contains), action type (Grant, Mask, Filter, Share, Group, FilterRule), and/or state (Active, Inactive, Deleted). All filters are optional and can be combined. Returns a paginated list — use the returned cursor to fetch subsequent pages.", - Handler: handleSearchDataAccessControls(collibraClient), - Permissions: []string{}, - } -} - -func handleSearchDataAccessControls(collibraClient *http.Client) chip.ToolHandlerFunc[SearchDataAccessControlsInput, SearchDataAccessControlsOutput] { - return func(ctx context.Context, input SearchDataAccessControlsInput) (SearchDataAccessControlsOutput, error) { - result, err := clients.SearchDataAccessControls(ctx, collibraClient, input.Name, input.Actions, input.States, input.Cursor, input.PageSize) - if err != nil { - return SearchDataAccessControlsOutput{ - Error: fmt.Sprintf("Failed to search data access controls: %s", err.Error()), - }, nil - } - - return SearchDataAccessControlsOutput{ - Results: result.Items, - NextCursor: result.NextCursor, - }, nil - } -} - -func NewSearchDataAccessRolesTool(collibraClient *http.Client) *chip.Tool[SearchDataAccessRolesInput, SearchDataAccessControlsOutput] { - return &chip.Tool[SearchDataAccessRolesInput, SearchDataAccessControlsOutput]{ - Name: "search_data_access_roles", - Description: "Search for data access roles (Grant-type access controls) in Collibra Data Access. Results can be filtered by name (case-insensitive contains) and/or state (Active, Inactive, Deleted). All filters are optional and can be combined. Returns a paginated list — use the returned cursor to fetch subsequent pages.", - Handler: handleSearchDataAccessRoles(collibraClient), - Permissions: []string{}, - } -} - -func handleSearchDataAccessRoles(collibraClient *http.Client) chip.ToolHandlerFunc[SearchDataAccessRolesInput, SearchDataAccessControlsOutput] { - return func(ctx context.Context, input SearchDataAccessRolesInput) (SearchDataAccessControlsOutput, error) { - result, err := clients.SearchDataAccessControls(ctx, collibraClient, input.Name, []string{"Grant"}, input.States, input.Cursor, input.PageSize) - if err != nil { - return SearchDataAccessControlsOutput{ - Error: fmt.Sprintf("Failed to search data access roles: %s", err.Error()), - }, nil - } - - return SearchDataAccessControlsOutput{ - Results: result.Items, - NextCursor: result.NextCursor, - }, nil - } -} diff --git a/pkg/tools/search_data_access_identities.go b/pkg/tools/search_data_access_identities/tool.go similarity index 50% rename from pkg/tools/search_data_access_identities.go rename to pkg/tools/search_data_access_identities/tool.go index ad0fc64..e1cd8d4 100644 --- a/pkg/tools/search_data_access_identities.go +++ b/pkg/tools/search_data_access_identities/tool.go @@ -1,4 +1,4 @@ -package tools +package search_data_access_identities import ( "context" @@ -7,33 +7,33 @@ import ( "github.com/collibra/chip/pkg/chip" "github.com/collibra/chip/pkg/clients" + "github.com/modelcontextprotocol/go-sdk/mcp" ) type SearchDataAccessIdentitiesInput struct { Email string `json:"email,omitempty" jsonschema:"Optional. Exact email address to look up the user by."` - Name string `json:"name,omitempty" jsonschema:"Optional. Search string for a case-insensitive contains match on the user's display name. When used without email, SearchUsers is called server-side. When used with email, it is applied as a client-side filter on the result."` - Cursor string `json:"cursor,omitempty" jsonschema:"Optional. Cursor from a previous response to fetch the next page of results. Only applicable for name-based searches."` - PageSize int `json:"pageSize,omitempty" jsonschema:"Optional. Number of results per page (default: 25, max: 25). Only applicable for name-based searches."` + Name string `json:"name,omitempty" jsonschema:"Optional. Search string for a case-insensitive contains match on the user's display name. When used without email, ListUsers is called server-side. When used with email, it is applied as a client-side filter on the result."` + PageSize int `json:"pageSize,omitempty" jsonschema:"Optional. Maximum number of results to return (default: 25, max: 25). Only applicable for name-based searches."` } type SearchDataAccessIdentitiesOutput struct { - Results []*clients.DataAccessIdentity `json:"results" jsonschema:"The matching Data Access users."` - NextCursor *string `json:"nextCursor,omitempty" jsonschema:"Cursor to pass in the next request to fetch the following page. Only present for name-based searches with more results available."` - Error string `json:"error,omitempty" jsonschema:"Error message if the search could not be completed."` + Results []*clients.DataAccessIdentity `json:"results" jsonschema:"The matching Data Access users."` + Error string `json:"error,omitempty" jsonschema:"Error message if the search could not be completed."` } -func NewSearchDataAccessIdentitiesTool(collibraClient *http.Client) *chip.Tool[SearchDataAccessIdentitiesInput, SearchDataAccessIdentitiesOutput] { +func NewTool(collibraClient *http.Client) *chip.Tool[SearchDataAccessIdentitiesInput, SearchDataAccessIdentitiesOutput] { return &chip.Tool[SearchDataAccessIdentitiesInput, SearchDataAccessIdentitiesOutput]{ Name: "search_data_access_identities", - Description: "Search for Data Access users (identities) by name and/or email. Providing email performs an exact lookup; providing name performs a case-insensitive contains search via SearchUsers. Both can be combined: email resolves the user, name filters the result client-side. Name-only searches are paginated — use the returned cursor to fetch subsequent pages.", + Description: "Search for Data Access users (identities) by name and/or email. Providing email performs an exact lookup; providing name performs a case-insensitive contains search via ListUsers. Both can be combined: email resolves the user, name filters the result client-side.", Handler: handleSearchDataAccessIdentities(collibraClient), Permissions: []string{}, + Annotations: &mcp.ToolAnnotations{ReadOnlyHint: true, DestructiveHint: new(false)}, } } func handleSearchDataAccessIdentities(collibraClient *http.Client) chip.ToolHandlerFunc[SearchDataAccessIdentitiesInput, SearchDataAccessIdentitiesOutput] { return func(ctx context.Context, input SearchDataAccessIdentitiesInput) (SearchDataAccessIdentitiesOutput, error) { - result, err := clients.SearchDataAccessIdentities(ctx, collibraClient, input.Name, input.Email, input.Cursor, input.PageSize) + result, err := clients.SearchDataAccessIdentities(ctx, collibraClient, input.Name, input.Email, input.PageSize) if err != nil { return SearchDataAccessIdentitiesOutput{ Error: fmt.Sprintf("Failed to search Data Access identities: %s", err.Error()), @@ -41,8 +41,7 @@ func handleSearchDataAccessIdentities(collibraClient *http.Client) chip.ToolHand } return SearchDataAccessIdentitiesOutput{ - Results: result.Items, - NextCursor: result.NextCursor, + Results: result.Items, }, nil } } diff --git a/pkg/tools/search_data_access_objects/tool.go b/pkg/tools/search_data_access_objects/tool.go new file mode 100644 index 0000000..cf79e1a --- /dev/null +++ b/pkg/tools/search_data_access_objects/tool.go @@ -0,0 +1,51 @@ +package search_data_access_objects + +import ( + "context" + "fmt" + "net/http" + + "github.com/collibra/chip/pkg/chip" + "github.com/collibra/chip/pkg/clients" + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +type SearchDataAccessObjectsInput struct { + Name string `json:"name,omitempty" jsonschema:"Optional. Filter by name (case-insensitive contains match on data object name)."` + DataSources []string `json:"dataSources,omitempty" jsonschema:"Optional. Restrict to data objects belonging to one or more data sources (data source IDs)."` + Types []string `json:"types,omitempty" jsonschema:"Optional. Restrict to data objects of one or more types (e.g. table, column, schema, view)."` + Parents []string `json:"parents,omitempty" jsonschema:"Optional. Restrict to data objects whose direct parent matches one of the given data object IDs."` + Ancestors []string `json:"ancestors,omitempty" jsonschema:"Optional. Restrict to data objects whose ancestors include one of the given data object IDs."` + IncludeDeleted bool `json:"includeDeleted,omitempty" jsonschema:"Optional. If true, also includes data objects that no longer exist in the source. Defaults to false."` + PageSize int `json:"pageSize,omitempty" jsonschema:"Optional. Maximum number of results to return (default: 25, max: 25)."` +} + +type SearchDataAccessObjectsOutput struct { + Results []*clients.DataAccessObject `json:"results" jsonschema:"The matching data objects."` + Error string `json:"error,omitempty" jsonschema:"Error message if the search could not be completed."` +} + +func NewTool(collibraClient *http.Client) *chip.Tool[SearchDataAccessObjectsInput, SearchDataAccessObjectsOutput] { + return &chip.Tool[SearchDataAccessObjectsInput, SearchDataAccessObjectsOutput]{ + Name: "search_data_access_objects", + Description: "Search for data objects in Collibra Data Access. Data objects represent tables, columns, schemas, views, and other entities tracked in registered data sources. Filters can be combined: name (case-insensitive contains), dataSources (data source IDs), types (e.g. table, column), parents/ancestors (other data object IDs), and includeDeleted. Returns up to pageSize matches (default 25, max 25). Each result also includes its applicablePermissions — the source-system permissions (with name and description) that can be requested on the object.", + Handler: handleSearchDataAccessObjects(collibraClient), + Permissions: []string{}, + Annotations: &mcp.ToolAnnotations{ReadOnlyHint: true, DestructiveHint: new(false)}, + } +} + +func handleSearchDataAccessObjects(collibraClient *http.Client) chip.ToolHandlerFunc[SearchDataAccessObjectsInput, SearchDataAccessObjectsOutput] { + return func(ctx context.Context, input SearchDataAccessObjectsInput) (SearchDataAccessObjectsOutput, error) { + result, err := clients.SearchDataAccessObjects(ctx, collibraClient, input.Name, input.DataSources, input.Types, input.Parents, input.Ancestors, input.IncludeDeleted, input.PageSize) + if err != nil { + return SearchDataAccessObjectsOutput{ + Error: fmt.Sprintf("Failed to search data access objects: %s", err.Error()), + }, nil + } + + return SearchDataAccessObjectsOutput{ + Results: result.Items, + }, nil + } +} From 2a7fcfd0a6105c3858ef7527b709bc13addc27f0 Mon Sep 17 00:00:00 2001 From: Wouter Cordewiner <73988312+wouterc-collibra@users.noreply.github.com> Date: Mon, 18 May 2026 09:27:57 +0200 Subject: [PATCH 03/13] github.com/collibra/data-access-go-sdk v0.0.61 --- go.mod | 4 +--- go.sum | 2 ++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index e2e5ce4..50e8147 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/collibra/chip go 1.26.2 require ( - github.com/collibra/data-access-go-sdk v1.0.0 + github.com/collibra/data-access-go-sdk v0.0.61 github.com/google/go-querystring v1.1.0 github.com/google/jsonschema-go v0.4.2 github.com/google/uuid v1.6.0 @@ -12,8 +12,6 @@ require ( github.com/spf13/viper v1.21.0 ) -replace github.com/collibra/data-access-go-sdk => /Users/wouterc/w/data-access-go-sdk-mcp - require ( github.com/Khan/genqlient v0.8.2-0.20251028054717-8ddeeee0a15a // indirect github.com/Masterminds/semver/v3 v3.5.0 // indirect diff --git a/go.sum b/go.sum index 7d659b8..2d23b5d 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAw github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/collibra/data-access-go-sdk v0.0.61 h1:Swmvmx279BAmJfTBeaeBQDP3yDazhr5q8p0V3pN6G9M= +github.com/collibra/data-access-go-sdk v0.0.61/go.mod h1:JPsGzZNdbTekWeNifho8xHbFKdyw8G5LxECtUZxYyYI= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= From ded00a5f5c6921a5e76f5a92e8bc1e68885afc98 Mon Sep 17 00:00:00 2001 From: Wouter Cordewiner <73988312+wouterc-collibra@users.noreply.github.com> Date: Fri, 8 May 2026 16:33:40 +0200 Subject: [PATCH 04/13] Tools for Data Access --- SKILLS.md | 30 +- pkg/clients/data_access_client.go | 350 +++++++++++++++++++ pkg/tools/get_data_access_control_details.go | 46 +++ pkg/tools/search_data_access_controls.go | 81 +++++ pkg/tools/search_data_access_identities.go | 48 +++ 5 files changed, 554 insertions(+), 1 deletion(-) create mode 100644 pkg/clients/data_access_client.go create mode 100644 pkg/tools/get_data_access_control_details.go create mode 100644 pkg/tools/search_data_access_controls.go create mode 100644 pkg/tools/search_data_access_identities.go diff --git a/SKILLS.md b/SKILLS.md index 05ff1ef..5e1108c 100644 --- a/SKILLS.md +++ b/SKILLS.md @@ -83,6 +83,18 @@ These tools query the technical lineage graph — a map of all data objects and **`search_lineage_transformations`** *(specialized)* — Search for transformations by name. Only use when the user explicitly asks about a transformation by name. This is **not** a general entry point for lineage questions — start with `search_lineage_entities` instead. +### Data Access + +These tools query Collibra Data Access — the system that manages who can access what data, through grants, masks, filters, and groups. + +**`search_data_access_controls`** — Search for data access controls. All filters are optional and combinable: `name` (case-insensitive contains), `actions` (one or more of `Grant`, `Mask`, `Filter`, `Share`, `Group`, `FilterRule`), `states` (one or more of `Active`, `Inactive`, `Deleted`). Returns a paginated list (25 per page); pass the returned `nextCursor` to fetch subsequent pages. + +**`search_data_access_roles`** — Alias of `search_data_access_controls` restricted to `Grant`-type controls. Use this when the user asks specifically about roles or who has been granted access. Supports the same `name` and `states` filters; the `actions` filter is fixed to `Grant` and cannot be overridden. Returns a paginated list (25 per page); pass the returned `nextCursor` to fetch subsequent pages. + +**`get_data_access_control_details`** — Retrieve full details for a single data access control by its id. Use this when you already have an access control ID and need to inspect it. + +**`search_data_access_identities`** — Search for Data Access users (identities) by name and/or email. Providing `email` performs an exact lookup via `GetUserByEmail`. Providing `name` performs a server-side case-insensitive contains search via `SearchUsers`. Both can be combined: email resolves the user, name filters the result client-side. Name-only searches are paginated (25 per page) — use the returned `nextCursor` to fetch subsequent pages. + ### Data Contracts **`list_data_contract`** — List data contracts with cursor-based pagination. Filter by `manifestId`. Use this to find a contract's UUID. @@ -135,6 +147,22 @@ These tools query the technical lineage graph — a map of all data objects and 2. `get_lineage_downstream` → relations with consumer entity IDs 3. Summarize based on the graph structure — only call `get_lineage_entity` for the most relevant consumers, not all of them +### Find and inspect data access controls +1. `search_data_access_controls` with optional name/action/state filters → get matching controls and their IDs +2. `get_data_access_control_details` with a specific ID → full details including grant category, policy rule, timestamps + +### Find and inspect data access roles +1. `search_data_access_roles` with optional name/state filters → get matching controls and their IDs +2. `get_data_access_control_details` with a specific ID → full details including grant category, policy rule, timestamps + +### Find who has been granted access (roles) +1. `search_data_access_roles` with optional name/state filters → returns only Grant-type controls +2. `get_data_access_control_details` for any result ID → full grant details + +### Look up a Data Access user by email or name +1. `search_data_access_identities` with `email` → exact lookup, returns the user's id, display name, and type + — or with `name` → paginated server-side contains search across all users + ### Manage a data contract 1. `list_data_contract` to find the contract UUID 2. `pull_data_contract_manifest` to download, edit, then `push_data_contract_manifest` to update @@ -147,5 +175,5 @@ These tools query the technical lineage graph — a map of all data objects and - **UUIDs are required for most tools.** When you only have a name, start with `search_asset_keyword` or the natural language discovery tools to get the UUID first. - **`discover_data_assets` vs `search_asset_keyword`**: Prefer `discover_data_assets` for open-ended semantic questions; prefer `search_asset_keyword` when you know the exact name or need to filter by type/community/domain. - **Permissions**: `discover_data_assets` and `discover_business_glossary` require the `dgc.ai-copilot` permission. Classification tools require `dgc.classify` + `dgc.catalog`. If a tool fails with a permission error, let the user know which permission is needed. -- **Pagination**: `search_asset_keyword`, `list_asset_types`, `search_data_class`, and `search_data_classification_match` use `limit`/`offset`. `list_data_contract` and `get_asset_details` (for relations) use cursor-based pagination — carry the cursor from the previous response. Lineage tools (`search_lineage_entities`, `get_lineage_upstream`, `get_lineage_downstream`, `search_lineage_transformations`) also use cursor-based pagination. +- **Pagination**: `search_asset_keyword`, `list_asset_types`, `search_data_class`, and `search_data_classification_match` use `limit`/`offset`. `list_data_contract` and `get_asset_details` (for relations) use cursor-based pagination — carry the cursor from the previous response. Lineage tools (`search_lineage_entities`, `get_lineage_upstream`, `get_lineage_downstream`, `search_lineage_transformations`) and data access tools (`search_data_access_controls`, `search_data_access_roles`) also use cursor-based pagination. - **Error handling**: Validation errors are returned in the output `error` field (not as Go errors), so always check `error` and `success`/`found` fields in the response before using the data. diff --git a/pkg/clients/data_access_client.go b/pkg/clients/data_access_client.go new file mode 100644 index 0000000..4bff19a --- /dev/null +++ b/pkg/clients/data_access_client.go @@ -0,0 +1,350 @@ +package clients + +import ( + "context" + "errors" + "fmt" + "net/http" + "strings" + "time" + + "github.com/collibra/chip/pkg/chip" + sdk "github.com/collibra/data-access-go-sdk" + "github.com/collibra/data-access-go-sdk/services" + "github.com/collibra/data-access-go-sdk/types" +) + +// DataAccessControlDetails holds the details of a single data access control. +type DataAccessControlDetails struct { + ID string `json:"id" jsonschema:"Unique identifier of the access control"` + Name string `json:"name" jsonschema:"Name of the access control"` + Description string `json:"description" jsonschema:"Detailed description of the access control"` + State string `json:"state" jsonschema:"State of the access control: ACTIVE, INACTIVE, or DELETED"` + Action string `json:"action" jsonschema:"Action type of the access control: GRANT, MASK, FILTER, SHARE, GROUP, or FILTERRULE"` + Category *DataAccessGrantCategory `json:"category,omitempty" jsonschema:"Grant category details, present only for GRANT action type"` + External bool `json:"external" jsonschema:"Whether the access control is managed externally in the data source rather than in Collibra Data Access"` + NamingHint *string `json:"namingHint,omitempty" jsonschema:"Naming hint used for generating names in target systems"` + PolicyRule *string `json:"policyRule,omitempty" jsonschema:"Policy rule string, used for imported row-level filters and column masks"` + NotInternalizable bool `json:"notInternalizable" jsonschema:"Whether the external access control cannot be internalized"` + Complete *bool `json:"complete,omitempty" jsonschema:"Whether the external access control is complete (all linked entities known in Collibra Data Access)"` + WhatUnknown bool `json:"whatUnknown" jsonschema:"Whether the WHAT scope of this access control could not be parsed on import"` + WhoUnknown bool `json:"whoUnknown" jsonschema:"Whether the WHO scope of this access control could not be parsed on import"` + CreatedAt time.Time `json:"createdAt" jsonschema:"Timestamp when the access control was created"` + ModifiedAt time.Time `json:"modifiedAt" jsonschema:"Timestamp when the access control was last modified"` + What []DataAccessWhatItem `json:"what" jsonschema:"List of access controls that this control applies to (the WHAT scope)"` + Who []DataAccessWhoItem `json:"who" jsonschema:"List of principals (users, access controls, data sources) that are granted access by this control"` + SyncData []DataAccessSyncData `json:"syncData" jsonschema:"Synchronization status per linked data source. Valid sync statuses: Notconnected, Failed, Outofdate, Inprogress, Synced, Outofsync."` +} + +// DataAccessSyncData holds the sync status of an access control for a single data source. +type DataAccessSyncData struct { + DataSourceID string `json:"dataSourceId" jsonschema:"Unique identifier of the linked data source"` + DataSourceName string `json:"dataSourceName" jsonschema:"Name of the linked data source"` + SyncStatus string `json:"syncStatus" jsonschema:"Sync status for this data source. Valid values: Notconnected, Failed, Outofdate, Inprogress, Synced, Outofsync."` +} + +// DataAccessWhatItem represents a single entry in the WHAT list of an access control — +// another access control that this one applies to. +type DataAccessWhatItem struct { + ID string `json:"id" jsonschema:"Unique identifier of the access control in the WHAT list"` + Name string `json:"name" jsonschema:"Name of the access control in the WHAT list"` + State string `json:"state" jsonschema:"State of the access control: Active, Inactive, or Deleted"` + Action string `json:"action" jsonschema:"Action type of the access control: Grant, Mask, Filter, Share, Group, or FilterRule"` + ExpiresAt *time.Time `json:"expiresAt,omitempty" jsonschema:"Optional expiration time for this WHAT entry"` +} + +// DataAccessWhoItem represents a single entry in the WHO list of an access control. +type DataAccessWhoItem struct { + // Type is either "WhoGrant" (direct access) or "WhoPromise" (pre-approved access on request). + Type string `json:"type" jsonschema:"Grant type: WhoGrant (direct access) or WhoPromise (pre-approved on request)"` + ExpiresAt *time.Time `json:"expiresAt,omitempty" jsonschema:"Optional expiration time for this WHO entry"` + PromiseDuration *int64 `json:"promiseDuration,omitempty" jsonschema:"For WhoPromise: duration in seconds of the grant when access is requested"` + // ItemType is the GraphQL typename of the item: User, AccessControl, DataShareRecipient, DataSource. + ItemType string `json:"itemType" jsonschema:"Type of the granted principal: User, AccessControl, DataShareRecipient, or DataSource"` + ItemID string `json:"itemId,omitempty" jsonschema:"ID of the granted principal (present for User and AccessControl item types)"` + ItemName string `json:"itemName,omitempty" jsonschema:"Display name of the granted principal (present for User and AccessControl item types)"` + Email *string `json:"email,omitempty" jsonschema:"Email address of the user (present for User item type only)"` + UserType string `json:"userType,omitempty" jsonschema:"Whether the user is a Human or Machine user (present for User item type only)"` +} + +// DataAccessGrantCategory holds the details of a grant category. +type DataAccessGrantCategory struct { + ID string `json:"id" jsonschema:"Unique identifier of the grant category"` + Name string `json:"name" jsonschema:"Display name of the grant category"` + NamePlural string `json:"namePlural" jsonschema:"Plural display name of the grant category"` + IsSystem bool `json:"isSystem" jsonschema:"Whether this grant category is system-defined and cannot be edited or removed"` + IsDefault bool `json:"isDefault" jsonschema:"Whether this is the default grant category for new access controls"` +} + +// GetDataAccessControl retrieves a single data access control by ID. +// It creates an sdk.CollibraClient using chip's existing HTTP client via sdk.WithHTTPClient, +// so URL routing and authentication are handled by chip's RoundTripper. +func GetDataAccessControl(ctx context.Context, httpClient *http.Client, id string) (*DataAccessControlDetails, error) { + collibraHost, ok := chip.GetCollibraHost(ctx) + if !ok { + return nil, fmt.Errorf("collibra host not configured in context") + } + dataAccessURL := strings.TrimSuffix(collibraHost, "/") + "/dataAccess" + + collibraClient, err := sdk.NewClient(dataAccessURL, sdk.WithHTTPClient(httpClient)) + if err != nil { + return nil, fmt.Errorf("failed to create data access client: %w", err) + } + + accessControlClient := collibraClient.AccessControl() + + ac, err := accessControlClient.GetAccessControl(ctx, id) + if err != nil { + return nil, err + } + + details := mapToDataAccessControlDetails(ac) + + for whatItem, err := range accessControlClient.GetAccessControlWhatAccessControlList(ctx, id) { + if err != nil { + return nil, fmt.Errorf("failed to retrieve what list: %w", err) + } + details.What = append(details.What, mapToDataAccessWhatItem(whatItem)) + } + + for whoItem, err := range accessControlClient.GetAccessControlWhoList(ctx, id) { + if err != nil { + return nil, fmt.Errorf("failed to retrieve who list: %w", err) + } + details.Who = append(details.Who, mapToDataAccessWhoItem(whoItem)) + } + + return details, nil +} + +// SearchDataAccessControlsResult holds a page of access controls and an optional next-page cursor. +type SearchDataAccessControlsResult struct { + Items []*DataAccessControlDetails `json:"items"` + NextCursor *string `json:"nextCursor,omitempty"` +} + +// SearchDataAccessControls returns a page of data access controls filtered by name, actions, and/or states. +// Name search is case-insensitive contains. Pass cursor from a previous response to fetch the next page. +func SearchDataAccessControls(ctx context.Context, httpClient *http.Client, name string, actions []string, states []string, cursor string, pageSize int) (*SearchDataAccessControlsResult, error) { + collibraHost, ok := chip.GetCollibraHost(ctx) + if !ok { + return nil, fmt.Errorf("collibra host not configured in context") + } + dataAccessURL := strings.TrimSuffix(collibraHost, "/") + "/dataAccess" + + collibraClient, err := sdk.NewClient(dataAccessURL, sdk.WithHTTPClient(httpClient)) + if err != nil { + return nil, fmt.Errorf("failed to create data access client: %w", err) + } + + filter := &types.AccessControlFilterInput{} + if name != "" { + filter.Search = &name + } + for _, a := range actions { + filter.Actions = append(filter.Actions, types.AccessControlAction(a)) + } + for _, s := range states { + filter.States = append(filter.States, types.AccessControlState(s)) + } + + opts := []func(*services.AccessControlListOptions){ + services.WithAccessControlListFilter(filter), + } + if cursor != "" { + opts = append(opts, services.WithAccessControlListCursor(cursor)) + } + if pageSize > 0 { + opts = append(opts, services.WithAccessControlListPageSize(pageSize)) + } + + items, nextCursor, err := collibraClient.AccessControl().ListAccessControlsPage(ctx, opts...) + if err != nil { + return nil, err + } + + result := &SearchDataAccessControlsResult{ + Items: make([]*DataAccessControlDetails, 0, len(items)), + NextCursor: nextCursor, + } + for _, ac := range items { + result.Items = append(result.Items, mapToDataAccessControlDetails(ac)) + } + return result, nil +} + +func mapToDataAccessWhatItem(w *types.AccessWhatAccessControlItem) DataAccessWhatItem { + item := DataAccessWhatItem{ + ExpiresAt: w.ExpiresAt, + } + if w.AccessControl != nil { + item.ID = w.AccessControl.AccessControl.Id + item.Name = w.AccessControl.AccessControl.Name + item.State = string(w.AccessControl.AccessControl.State) + item.Action = string(w.AccessControl.AccessControl.Action) + } + return item +} + +func mapToDataAccessWhoItem(w *types.AccessWhoItem) DataAccessWhoItem { + item := DataAccessWhoItem{ + Type: string(w.Type), + ExpiresAt: w.ExpiresAt, + PromiseDuration: w.PromiseDuration, + } + + switch v := w.Item.(type) { + case *types.AccessWhoItemItemUser: + item.ItemType = "User" + item.ItemID = v.User.Id + item.ItemName = v.User.Name + item.Email = v.User.Email + item.UserType = string(v.User.Type) + case *types.AccessWhoItemItemAccessControl: + item.ItemType = "AccessControl" + item.ItemID = v.Id + item.ItemName = v.Name + case *types.AccessWhoItemItemDataShareRecipient: + item.ItemType = "DataShareRecipient" + case *types.AccessWhoItemItemDataSource: + item.ItemType = "DataSource" + default: + if v != nil { + if t := w.Item.GetTypename(); t != nil { + item.ItemType = *t + } + } + } + + return item +} + +// DataAccessIdentity represents a user in Collibra Data Access. +type DataAccessIdentity struct { + ID string `json:"id" jsonschema:"Unique identifier of the user"` + Name string `json:"name" jsonschema:"Display name of the user"` + Email *string `json:"email,omitempty" jsonschema:"Email address of the user"` + Type string `json:"type" jsonschema:"User type: Human or Machine"` +} + +// SearchDataAccessIdentitiesResult holds a page of identities and an optional next-page cursor. +type SearchDataAccessIdentitiesResult struct { + Items []*DataAccessIdentity + NextCursor *string +} + +// SearchDataAccessIdentities searches for Data Access users by name and/or email. +// When email is provided, an exact lookup via GetUserByEmail is performed. Name is then applied +// as an optional client-side case-insensitive contains filter on the result. +// When only name is provided, SearchUsers is called with the Search filter (case-insensitive +// contains). Cursor and pageSize control pagination for name-based searches. +func SearchDataAccessIdentities(ctx context.Context, httpClient *http.Client, name, email, cursor string, pageSize int) (*SearchDataAccessIdentitiesResult, error) { + collibraHost, ok := chip.GetCollibraHost(ctx) + if !ok { + return nil, fmt.Errorf("collibra host not configured in context") + } + dataAccessURL := strings.TrimSuffix(collibraHost, "/") + "/dataAccess" + + collibraClient, err := sdk.NewClient(dataAccessURL, sdk.WithHTTPClient(httpClient)) + if err != nil { + return nil, fmt.Errorf("failed to create data access client: %w", err) + } + + if email != "" { + user, err := collibraClient.User().GetUserByEmail(ctx, email) + if err != nil { + var notFound *types.ErrNotFound + if errors.As(err, ¬Found) { + return &SearchDataAccessIdentitiesResult{Items: []*DataAccessIdentity{}}, nil + } + return nil, err + } + + identity := mapToDataAccessIdentity(user) + if name != "" && !strings.Contains(strings.ToLower(identity.Name), strings.ToLower(name)) { + return &SearchDataAccessIdentitiesResult{Items: []*DataAccessIdentity{}}, nil + } + return &SearchDataAccessIdentitiesResult{Items: []*DataAccessIdentity{identity}}, nil + } + + // Name-only path: use SearchUsers with the Search filter. + filter := &types.UserFilterInput{} + if name != "" { + filter.Search = &name + } + + var after *string + if cursor != "" { + after = &cursor + } + var limit *int + if pageSize > 0 { + limit = &pageSize + } + + users, nextCursor, err := collibraClient.User().SearchUsers(ctx, after, limit, filter) + if err != nil { + return nil, err + } + + result := &SearchDataAccessIdentitiesResult{ + Items: make([]*DataAccessIdentity, 0, len(users)), + NextCursor: nextCursor, + } + for i := range users { + result.Items = append(result.Items, mapToDataAccessIdentity(&users[i])) + } + return result, nil +} + +func mapToDataAccessIdentity(u *types.User) *DataAccessIdentity { + return &DataAccessIdentity{ + ID: u.Id, + Name: u.Name, + Email: u.Email, + Type: string(u.Type), + } +} + +func mapToDataAccessControlDetails(ac *types.AccessControl) *DataAccessControlDetails { + details := &DataAccessControlDetails{ + What: []DataAccessWhatItem{}, + Who: []DataAccessWhoItem{}, + SyncData: []DataAccessSyncData{}, + ID: ac.Id, + Name: ac.Name, + Description: ac.Description, + State: string(ac.State), + Action: string(ac.Action), + External: ac.External, + NamingHint: ac.NamingHint, + PolicyRule: ac.PolicyRule, + NotInternalizable: ac.NotInternalizable, + Complete: ac.Complete, + WhatUnknown: ac.WhatUnknown, + WhoUnknown: ac.WhoUnknown, + CreatedAt: ac.CreatedAt, + ModifiedAt: ac.ModifiedAt, + } + + if ac.Category != nil { + details.Category = &DataAccessGrantCategory{ + ID: ac.Category.GrantCategory.Id, + Name: ac.Category.GrantCategory.Name, + NamePlural: ac.Category.GrantCategory.NamePlural, + IsSystem: ac.Category.GrantCategory.IsSystem, + IsDefault: ac.Category.GrantCategory.IsDefault, + } + } + + for _, sd := range ac.SyncData { + ds := sd.GetDataSource() + details.SyncData = append(details.SyncData, DataAccessSyncData{ + DataSourceID: ds.GetId(), + DataSourceName: ds.GetName(), + SyncStatus: string(sd.GetSyncStatus()), + }) + } + + return details +} diff --git a/pkg/tools/get_data_access_control_details.go b/pkg/tools/get_data_access_control_details.go new file mode 100644 index 0000000..a48b0f1 --- /dev/null +++ b/pkg/tools/get_data_access_control_details.go @@ -0,0 +1,46 @@ +package tools + +import ( + "context" + "fmt" + "net/http" + + "github.com/collibra/chip/pkg/chip" + "github.com/collibra/chip/pkg/clients" +) + +type DataAccessControlInput struct { + ID string `json:"id" jsonschema:"The id of the data access control to retrieve"` +} + +type DataAccessControlOutput struct { + AccessControl *clients.DataAccessControlDetails `json:"accessControl,omitempty" jsonschema:"The data access control details if found"` + Error string `json:"error,omitempty" jsonschema:"Error message if the access control could not be retrieved"` + Found bool `json:"found" jsonschema:"Whether the data access control was found"` +} + +func NewGetDataAccessControlDetailsTool(collibraClient *http.Client) *chip.Tool[DataAccessControlInput, DataAccessControlOutput] { + return &chip.Tool[DataAccessControlInput, DataAccessControlOutput]{ + Name: "get_data_access_control_details", + Description: "Retrieve detailed information about a specific Collibra Data Access control by its id. Returns the access control's name, description, state (ACTIVE, INACTIVE, DELETED), action type (GRANT, MASK, FILTER, SHARE, GROUP, FILTERRULE), grant category, policy rule, external management status, ABAC scope parse status, and timestamps. Use this to inspect an individual access control when you know its ID.", + Handler: handleGetDataAccessControlDetails(collibraClient), + Permissions: []string{}, + } +} + +func handleGetDataAccessControlDetails(collibraClient *http.Client) chip.ToolHandlerFunc[DataAccessControlInput, DataAccessControlOutput] { + return func(ctx context.Context, input DataAccessControlInput) (DataAccessControlOutput, error) { + details, err := clients.GetDataAccessControl(ctx, collibraClient, input.ID) + if err != nil { + return DataAccessControlOutput{ + Error: fmt.Sprintf("Failed to retrieve data access control: %s", err.Error()), + Found: false, + }, nil + } + + return DataAccessControlOutput{ + AccessControl: details, + Found: true, + }, nil + } +} diff --git a/pkg/tools/search_data_access_controls.go b/pkg/tools/search_data_access_controls.go new file mode 100644 index 0000000..53eca41 --- /dev/null +++ b/pkg/tools/search_data_access_controls.go @@ -0,0 +1,81 @@ +package tools + +import ( + "context" + "fmt" + "net/http" + + "github.com/collibra/chip/pkg/chip" + "github.com/collibra/chip/pkg/clients" +) + +type SearchDataAccessControlsInput struct { + Name string `json:"name,omitempty" jsonschema:"Optional. Filter by name (case-insensitive contains match)."` + Actions []string `json:"actions,omitempty" jsonschema:"Optional. Filter by one or more action types. Valid values: Grant, Mask, Filter, Share, Group, FilterRule."` + States []string `json:"states,omitempty" jsonschema:"Optional. Filter by one or more states. Valid values: Active, Inactive, Deleted."` + Cursor string `json:"cursor,omitempty" jsonschema:"Optional. Cursor from a previous response to fetch the next page of results."` + PageSize int `json:"pageSize,omitempty" jsonschema:"Optional. Number of results per page (default: 25, max: 25)."` +} + +type SearchDataAccessRolesInput struct { + Name string `json:"name,omitempty" jsonschema:"Optional. Filter by name (case-insensitive contains match)."` + States []string `json:"states,omitempty" jsonschema:"Optional. Filter by one or more states. Valid values: Active, Inactive, Deleted."` + Cursor string `json:"cursor,omitempty" jsonschema:"Optional. Cursor from a previous response to fetch the next page of results."` + PageSize int `json:"pageSize,omitempty" jsonschema:"Optional. Number of results per page (default: 25, max: 25)."` +} + +type SearchDataAccessControlsOutput struct { + Results []*clients.DataAccessControlDetails `json:"results" jsonschema:"The matching data access controls."` + NextCursor *string `json:"nextCursor,omitempty" jsonschema:"Cursor to pass in the next request to fetch the following page. Absent when there are no more results."` + Error string `json:"error,omitempty" jsonschema:"Error message if the search could not be completed."` +} + +func NewSearchDataAccessControlsTool(collibraClient *http.Client) *chip.Tool[SearchDataAccessControlsInput, SearchDataAccessControlsOutput] { + return &chip.Tool[SearchDataAccessControlsInput, SearchDataAccessControlsOutput]{ + Name: "search_data_access_controls", + Description: "Search for data access controls in Collibra Data Access. Results can be filtered by name (case-insensitive contains), action type (Grant, Mask, Filter, Share, Group, FilterRule), and/or state (Active, Inactive, Deleted). All filters are optional and can be combined. Returns a paginated list — use the returned cursor to fetch subsequent pages.", + Handler: handleSearchDataAccessControls(collibraClient), + Permissions: []string{}, + } +} + +func handleSearchDataAccessControls(collibraClient *http.Client) chip.ToolHandlerFunc[SearchDataAccessControlsInput, SearchDataAccessControlsOutput] { + return func(ctx context.Context, input SearchDataAccessControlsInput) (SearchDataAccessControlsOutput, error) { + result, err := clients.SearchDataAccessControls(ctx, collibraClient, input.Name, input.Actions, input.States, input.Cursor, input.PageSize) + if err != nil { + return SearchDataAccessControlsOutput{ + Error: fmt.Sprintf("Failed to search data access controls: %s", err.Error()), + }, nil + } + + return SearchDataAccessControlsOutput{ + Results: result.Items, + NextCursor: result.NextCursor, + }, nil + } +} + +func NewSearchDataAccessRolesTool(collibraClient *http.Client) *chip.Tool[SearchDataAccessRolesInput, SearchDataAccessControlsOutput] { + return &chip.Tool[SearchDataAccessRolesInput, SearchDataAccessControlsOutput]{ + Name: "search_data_access_roles", + Description: "Search for data access roles (Grant-type access controls) in Collibra Data Access. Results can be filtered by name (case-insensitive contains) and/or state (Active, Inactive, Deleted). All filters are optional and can be combined. Returns a paginated list — use the returned cursor to fetch subsequent pages.", + Handler: handleSearchDataAccessRoles(collibraClient), + Permissions: []string{}, + } +} + +func handleSearchDataAccessRoles(collibraClient *http.Client) chip.ToolHandlerFunc[SearchDataAccessRolesInput, SearchDataAccessControlsOutput] { + return func(ctx context.Context, input SearchDataAccessRolesInput) (SearchDataAccessControlsOutput, error) { + result, err := clients.SearchDataAccessControls(ctx, collibraClient, input.Name, []string{"Grant"}, input.States, input.Cursor, input.PageSize) + if err != nil { + return SearchDataAccessControlsOutput{ + Error: fmt.Sprintf("Failed to search data access roles: %s", err.Error()), + }, nil + } + + return SearchDataAccessControlsOutput{ + Results: result.Items, + NextCursor: result.NextCursor, + }, nil + } +} diff --git a/pkg/tools/search_data_access_identities.go b/pkg/tools/search_data_access_identities.go new file mode 100644 index 0000000..ad0fc64 --- /dev/null +++ b/pkg/tools/search_data_access_identities.go @@ -0,0 +1,48 @@ +package tools + +import ( + "context" + "fmt" + "net/http" + + "github.com/collibra/chip/pkg/chip" + "github.com/collibra/chip/pkg/clients" +) + +type SearchDataAccessIdentitiesInput struct { + Email string `json:"email,omitempty" jsonschema:"Optional. Exact email address to look up the user by."` + Name string `json:"name,omitempty" jsonschema:"Optional. Search string for a case-insensitive contains match on the user's display name. When used without email, SearchUsers is called server-side. When used with email, it is applied as a client-side filter on the result."` + Cursor string `json:"cursor,omitempty" jsonschema:"Optional. Cursor from a previous response to fetch the next page of results. Only applicable for name-based searches."` + PageSize int `json:"pageSize,omitempty" jsonschema:"Optional. Number of results per page (default: 25, max: 25). Only applicable for name-based searches."` +} + +type SearchDataAccessIdentitiesOutput struct { + Results []*clients.DataAccessIdentity `json:"results" jsonschema:"The matching Data Access users."` + NextCursor *string `json:"nextCursor,omitempty" jsonschema:"Cursor to pass in the next request to fetch the following page. Only present for name-based searches with more results available."` + Error string `json:"error,omitempty" jsonschema:"Error message if the search could not be completed."` +} + +func NewSearchDataAccessIdentitiesTool(collibraClient *http.Client) *chip.Tool[SearchDataAccessIdentitiesInput, SearchDataAccessIdentitiesOutput] { + return &chip.Tool[SearchDataAccessIdentitiesInput, SearchDataAccessIdentitiesOutput]{ + Name: "search_data_access_identities", + Description: "Search for Data Access users (identities) by name and/or email. Providing email performs an exact lookup; providing name performs a case-insensitive contains search via SearchUsers. Both can be combined: email resolves the user, name filters the result client-side. Name-only searches are paginated — use the returned cursor to fetch subsequent pages.", + Handler: handleSearchDataAccessIdentities(collibraClient), + Permissions: []string{}, + } +} + +func handleSearchDataAccessIdentities(collibraClient *http.Client) chip.ToolHandlerFunc[SearchDataAccessIdentitiesInput, SearchDataAccessIdentitiesOutput] { + return func(ctx context.Context, input SearchDataAccessIdentitiesInput) (SearchDataAccessIdentitiesOutput, error) { + result, err := clients.SearchDataAccessIdentities(ctx, collibraClient, input.Name, input.Email, input.Cursor, input.PageSize) + if err != nil { + return SearchDataAccessIdentitiesOutput{ + Error: fmt.Sprintf("Failed to search Data Access identities: %s", err.Error()), + }, nil + } + + return SearchDataAccessIdentitiesOutput{ + Results: result.Items, + NextCursor: result.NextCursor, + }, nil + } +} From 83085f9b9beac7431572c025a56dc31ddd57acc7 Mon Sep 17 00:00:00 2001 From: Wouter Cordewiner <73988312+wouterc-collibra@users.noreply.github.com> Date: Fri, 15 May 2026 10:48:19 +0200 Subject: [PATCH 05/13] Tools for Data Access --- SKILLS.md | 25 +- go.mod | 12 +- go.sum | 37 ++- pkg/clients/data_access_client.go | 264 +++++++++++++----- pkg/tools/create_data_access_request/tool.go | 131 +++++++++ .../tool.go} | 6 +- pkg/tools/register.go | 12 +- pkg/tools/search_data_access_controls.go | 81 ------ .../tool.go} | 23 +- pkg/tools/search_data_access_objects/tool.go | 51 ++++ 10 files changed, 454 insertions(+), 188 deletions(-) create mode 100644 pkg/tools/create_data_access_request/tool.go rename pkg/tools/{get_data_access_control_details.go => get_data_access_control_details/tool.go} (87%) delete mode 100644 pkg/tools/search_data_access_controls.go rename pkg/tools/{search_data_access_identities.go => search_data_access_identities/tool.go} (50%) create mode 100644 pkg/tools/search_data_access_objects/tool.go diff --git a/SKILLS.md b/SKILLS.md index 5e1108c..2261928 100644 --- a/SKILLS.md +++ b/SKILLS.md @@ -87,13 +87,17 @@ These tools query the technical lineage graph — a map of all data objects and These tools query Collibra Data Access — the system that manages who can access what data, through grants, masks, filters, and groups. -**`search_data_access_controls`** — Search for data access controls. All filters are optional and combinable: `name` (case-insensitive contains), `actions` (one or more of `Grant`, `Mask`, `Filter`, `Share`, `Group`, `FilterRule`), `states` (one or more of `Active`, `Inactive`, `Deleted`). Returns a paginated list (25 per page); pass the returned `nextCursor` to fetch subsequent pages. +**`search_data_access_identities`** — Search for Data Access users (identities) by name and/or email. Providing `email` performs an exact lookup via `GetUserByEmail`. Providing `name` performs a server-side case-insensitive contains search via `SearchUsers`. Both can be combined: email resolves the user, name filters the result client-side. Name-only searches are paginated (25 per page) — use the returned `nextCursor` to fetch subsequent pages. -**`search_data_access_roles`** — Alias of `search_data_access_controls` restricted to `Grant`-type controls. Use this when the user asks specifically about roles or who has been granted access. Supports the same `name` and `states` filters; the `actions` filter is fixed to `Grant` and cannot be overridden. Returns a paginated list (25 per page); pass the returned `nextCursor` to fetch subsequent pages. +**`search_data_access_objects`** — Search for data objects in Collibra Data Access (tables, columns, schemas, views, and other entities tracked in registered data sources). Filters can be combined: `name` (case-insensitive contains), `dataSources` (data source IDs), `types` (e.g. `table`, `column`, `schema`, `view`), `parents` / `ancestors` (other data object IDs to scope the search to a sub-tree), and `includeDeleted`. Returns up to `pageSize` matches (default 25, max 25). Each result includes the data object ID, name, fully qualified name, type, data type, deleted flag, description, data source ID, and `applicablePermissions` — the list of source-system permissions (each with a `name` and `description`) that can be requested on the object. Use those names when populating `what[].permissions` for `create_data_access_request`. -**`get_data_access_control_details`** — Retrieve full details for a single data access control by its id. Use this when you already have an access control ID and need to inspect it. +**`create_data_access_request`** — Create a new Collibra Data Access request on behalf of one or more users for one or more data objects. Destructive. Required behavior: -**`search_data_access_identities`** — Search for Data Access users (identities) by name and/or email. Providing `email` performs an exact lookup via `GetUserByEmail`. Providing `name` performs a server-side case-insensitive contains search via `SearchUsers`. Both can be combined: email resolves the user, name filters the result client-side. Name-only searches are paginated (25 per page) — use the returned `nextCursor` to fetch subsequent pages. +- **Minimum input is WHO, WHAT, and a purpose.** Do not call this tool until all three are supplied. +- **WHO** must be resolved via `search_data_access_identities` (by email or name) — pass the returned user IDs in `userIds`. Never pass raw emails or names. +- **WHAT** must be resolved via `search_data_access_objects` — pass the returned data object IDs in `what[].dataObjectId`. Per item, `permissions` should be empty and `globalPermissions` must always be READ. +- **Purpose** is mandatory and must come from the user — it is the business justification for the request. If the user has not stated a purpose, ask them for one before calling the tool. Do not invent a purpose. The tool always appends a note stating that the request was created by AI. +- **Name** is optional. If the user does not provide one, omit `name` on the first call. The tool will return status `needs_name_confirmation` with a `suggestedName` derived from the purpose — present that suggestion to the user, get their confirmation (or an alternative), and call again with the confirmed value in `name`. ### Data Contracts @@ -147,13 +151,8 @@ These tools query Collibra Data Access — the system that manages who can acces 2. `get_lineage_downstream` → relations with consumer entity IDs 3. Summarize based on the graph structure — only call `get_lineage_entity` for the most relevant consumers, not all of them -### Find and inspect data access controls -1. `search_data_access_controls` with optional name/action/state filters → get matching controls and their IDs -2. `get_data_access_control_details` with a specific ID → full details including grant category, policy rule, timestamps - ### Find and inspect data access roles -1. `search_data_access_roles` with optional name/state filters → get matching controls and their IDs -2. `get_data_access_control_details` with a specific ID → full details including grant category, policy rule, timestamps +1. `get_data_access_control_details` with a specific ID → full details including grant category, policy rule, timestamps ### Find who has been granted access (roles) 1. `search_data_access_roles` with optional name/state filters → returns only Grant-type controls @@ -163,6 +162,12 @@ These tools query Collibra Data Access — the system that manages who can acces 1. `search_data_access_identities` with `email` → exact lookup, returns the user's id, display name, and type — or with `name` → paginated server-side contains search across all users +### Create a Data Access request +1. Make sure the user has stated a `purpose` — the business justification for the request. If missing, ask for it before continuing. +2. `search_data_access_identities` for every beneficiary → collect the user IDs (the WHO) +3. `search_data_access_objects` for every data object the users need → collect the data object IDs (the WHAT) +4. `create_data_access_request` with `purpose`, `userIds`, and `what` — if the user has not provided a name, omit `name`. The tool returns `needs_name_confirmation` with a `suggestedName` derived from the purpose; confirm it with the user, then call again with `name` set. The purpose is used as the description, with an AI-created note appended automatically. + ### Manage a data contract 1. `list_data_contract` to find the contract UUID 2. `pull_data_contract_manifest` to download, edit, then `push_data_contract_manifest` to update diff --git a/go.mod b/go.mod index 75e64f0..e2e5ce4 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,9 @@ module github.com/collibra/chip -go 1.25.0 +go 1.26.2 require ( + github.com/collibra/data-access-go-sdk v1.0.0 github.com/google/go-querystring v1.1.0 github.com/google/jsonschema-go v0.4.2 github.com/google/uuid v1.6.0 @@ -11,9 +12,15 @@ require ( github.com/spf13/viper v1.21.0 ) +replace github.com/collibra/data-access-go-sdk => /Users/wouterc/w/data-access-go-sdk-mcp + require ( + github.com/Khan/genqlient v0.8.2-0.20251028054717-8ddeeee0a15a // indirect + github.com/Masterminds/semver/v3 v3.5.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-retryablehttp v0.7.8 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/sagikazarmark/locafero v0.12.0 // indirect @@ -22,10 +29,11 @@ require ( github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/vektah/gqlparser/v2 v2.5.30 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/oauth2 v0.35.0 // indirect golang.org/x/sys v0.41.0 // indirect golang.org/x/text v0.32.0 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index c47ee63..7d659b8 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,13 @@ -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/Khan/genqlient v0.8.2-0.20251028054717-8ddeeee0a15a h1:kx/iDWW6lRgNBqTL1z6UB7ajcZR3OcVLKVJ39QkUnUw= +github.com/Khan/genqlient v0.8.2-0.20251028054717-8ddeeee0a15a/go.mod h1:guUHwMi8ByjIvs3TyAPM+V9ryaW305CtK7+aCeP2Jzc= +github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= +github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= @@ -17,19 +25,26 @@ github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbc github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= +github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/modelcontextprotocol/go-sdk v1.5.0 h1:CHU0FIX9kpueNkxuYtfYQn1Z0slhFzBZuq+x6IiblIU= github.com/modelcontextprotocol/go-sdk v1.5.0/go.mod h1:gggDIhoemhWs3BGkGwd1umzEXCEMMvAnhTrnbXJKKKA= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= @@ -38,6 +53,8 @@ github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc= github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg= github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0= github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0= +github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= @@ -50,6 +67,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/vektah/gqlparser/v2 v2.5.30 h1:EqLwGAFLIzt1wpx1IPpY67DwUujF1OfzgEyDsLrN6kE= +github.com/vektah/gqlparser/v2 v2.5.30/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= @@ -60,8 +79,8 @@ golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= -golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= -golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/pkg/clients/data_access_client.go b/pkg/clients/data_access_client.go index 4bff19a..1cb02a8 100644 --- a/pkg/clients/data_access_client.go +++ b/pkg/clients/data_access_client.go @@ -123,56 +123,6 @@ type SearchDataAccessControlsResult struct { NextCursor *string `json:"nextCursor,omitempty"` } -// SearchDataAccessControls returns a page of data access controls filtered by name, actions, and/or states. -// Name search is case-insensitive contains. Pass cursor from a previous response to fetch the next page. -func SearchDataAccessControls(ctx context.Context, httpClient *http.Client, name string, actions []string, states []string, cursor string, pageSize int) (*SearchDataAccessControlsResult, error) { - collibraHost, ok := chip.GetCollibraHost(ctx) - if !ok { - return nil, fmt.Errorf("collibra host not configured in context") - } - dataAccessURL := strings.TrimSuffix(collibraHost, "/") + "/dataAccess" - - collibraClient, err := sdk.NewClient(dataAccessURL, sdk.WithHTTPClient(httpClient)) - if err != nil { - return nil, fmt.Errorf("failed to create data access client: %w", err) - } - - filter := &types.AccessControlFilterInput{} - if name != "" { - filter.Search = &name - } - for _, a := range actions { - filter.Actions = append(filter.Actions, types.AccessControlAction(a)) - } - for _, s := range states { - filter.States = append(filter.States, types.AccessControlState(s)) - } - - opts := []func(*services.AccessControlListOptions){ - services.WithAccessControlListFilter(filter), - } - if cursor != "" { - opts = append(opts, services.WithAccessControlListCursor(cursor)) - } - if pageSize > 0 { - opts = append(opts, services.WithAccessControlListPageSize(pageSize)) - } - - items, nextCursor, err := collibraClient.AccessControl().ListAccessControlsPage(ctx, opts...) - if err != nil { - return nil, err - } - - result := &SearchDataAccessControlsResult{ - Items: make([]*DataAccessControlDetails, 0, len(items)), - NextCursor: nextCursor, - } - for _, ac := range items { - result.Items = append(result.Items, mapToDataAccessControlDetails(ac)) - } - return result, nil -} - func mapToDataAccessWhatItem(w *types.AccessWhatAccessControlItem) DataAccessWhatItem { item := DataAccessWhatItem{ ExpiresAt: w.ExpiresAt, @@ -227,18 +177,17 @@ type DataAccessIdentity struct { Type string `json:"type" jsonschema:"User type: Human or Machine"` } -// SearchDataAccessIdentitiesResult holds a page of identities and an optional next-page cursor. +// SearchDataAccessIdentitiesResult holds a page of identities. type SearchDataAccessIdentitiesResult struct { - Items []*DataAccessIdentity - NextCursor *string + Items []*DataAccessIdentity } // SearchDataAccessIdentities searches for Data Access users by name and/or email. // When email is provided, an exact lookup via GetUserByEmail is performed. Name is then applied // as an optional client-side case-insensitive contains filter on the result. -// When only name is provided, SearchUsers is called with the Search filter (case-insensitive -// contains). Cursor and pageSize control pagination for name-based searches. -func SearchDataAccessIdentities(ctx context.Context, httpClient *http.Client, name, email, cursor string, pageSize int) (*SearchDataAccessIdentitiesResult, error) { +// When only name is provided, ListUsers is called with the Search filter (case-insensitive +// contains). The returned list is capped at pageSize items (default 25). +func SearchDataAccessIdentities(ctx context.Context, httpClient *http.Client, name, email string, pageSize int) (*SearchDataAccessIdentitiesResult, error) { collibraHost, ok := chip.GetCollibraHost(ctx) if !ok { return nil, fmt.Errorf("collibra host not configured in context") @@ -267,36 +216,211 @@ func SearchDataAccessIdentities(ctx context.Context, httpClient *http.Client, na return &SearchDataAccessIdentitiesResult{Items: []*DataAccessIdentity{identity}}, nil } - // Name-only path: use SearchUsers with the Search filter. filter := &types.UserFilterInput{} if name != "" { filter.Search = &name } - var after *string - if cursor != "" { - after = &cursor + limit := pageSize + if limit <= 0 { + limit = 25 } - var limit *int - if pageSize > 0 { - limit = &pageSize + + result := &SearchDataAccessIdentitiesResult{ + Items: make([]*DataAccessIdentity, 0, limit), } + for user, iterErr := range collibraClient.User().ListUsers(ctx, services.WithUserListFilter(filter)) { + if iterErr != nil { + return nil, iterErr + } + result.Items = append(result.Items, mapToDataAccessIdentity(user)) + if len(result.Items) >= limit { + break + } + } + return result, nil +} - users, nextCursor, err := collibraClient.User().SearchUsers(ctx, after, limit, filter) +// DataAccessObject represents a single data object in Collibra Data Access. +type DataAccessObject struct { + ID string `json:"id" jsonschema:"Unique identifier of the data object"` + Name string `json:"name" jsonschema:"Name of the data object"` + FullName string `json:"fullName" jsonschema:"Fully qualified name of the data object within its data source"` + Type string `json:"type" jsonschema:"Type of the data object (e.g. table, column, schema, view)"` + DataType *string `json:"dataType,omitempty" jsonschema:"Data type of the object (typically used for columns)"` + Deleted bool `json:"deleted" jsonschema:"Whether the data object is deleted (no longer present in the source)"` + Description string `json:"description" jsonschema:"Description of the data object"` + DataSourceID string `json:"dataSourceId,omitempty" jsonschema:"Identifier of the data source the object belongs to"` + ApplicablePermissions []DataAccessPermission `json:"applicablePermissions,omitempty" jsonschema:"Source-system permissions that can be requested or granted on this data object (and its descendants). Each permission carries its name and description."` +} + +// DataAccessPermission is a permission that can be set on a data object. +type DataAccessPermission struct { + Name string `json:"name" jsonschema:"Permission name as defined by the data source (e.g. SELECT, INSERT)"` + Description string `json:"description" jsonschema:"Human-readable description of the permission"` +} + +// SearchDataAccessObjectsResult holds a page of data objects. +type SearchDataAccessObjectsResult struct { + Items []*DataAccessObject `json:"items"` +} + +// SearchDataAccessObjects returns a list of data objects matching the supplied filters. +// Name search is case-insensitive contains. The returned list is capped at pageSize items +// (default 25), drawn from the SDK's ListDataObjects iterator. +func SearchDataAccessObjects(ctx context.Context, httpClient *http.Client, name string, dataSources []string, dataObjectTypes []string, parents []string, ancestors []string, includeDeleted bool, pageSize int) (*SearchDataAccessObjectsResult, error) { + collibraHost, ok := chip.GetCollibraHost(ctx) + if !ok { + return nil, fmt.Errorf("collibra host not configured in context") + } + dataAccessURL := strings.TrimSuffix(collibraHost, "/") + "/dataAccess" + + collibraClient, err := sdk.NewClient(dataAccessURL, sdk.WithHTTPClient(httpClient)) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to create data access client: %w", err) } - result := &SearchDataAccessIdentitiesResult{ - Items: make([]*DataAccessIdentity, 0, len(users)), - NextCursor: nextCursor, + filter := &types.DataObjectFilterInput{} + if name != "" { + filter.Search = &name + } + if len(dataSources) > 0 { + filter.DataSources = dataSources + } + if len(dataObjectTypes) > 0 { + filter.Types = dataObjectTypes } - for i := range users { - result.Items = append(result.Items, mapToDataAccessIdentity(&users[i])) + if len(parents) > 0 { + filter.Parents = parents + } + if len(ancestors) > 0 { + filter.Ancestors = ancestors + } + if includeDeleted { + filter.IncludeDeleted = &includeDeleted + } + + limit := pageSize + if limit <= 0 { + limit = 25 + } + + result := &SearchDataAccessObjectsResult{ + Items: make([]*DataAccessObject, 0, limit), + } + for obj, iterErr := range collibraClient.DataObject().ListDataObjects(ctx, services.WithDataObjectListFilter(filter)) { + if iterErr != nil { + return nil, iterErr + } + result.Items = append(result.Items, mapToDataAccessObject(obj)) + if len(result.Items) >= limit { + break + } } return result, nil } +// CreateDataAccessRequestWhatInput describes a single WHAT item (a data object) for a new +// data access request, with optional requested permissions. +type CreateDataAccessRequestWhatInput struct { + DataObjectID string `json:"dataObjectId" jsonschema:"The ID of the data object the requesters want access to. Obtain via search_data_access_objects."` + Permissions []string `json:"permissions,omitempty" jsonschema:"Source-system permissions requested on this data object (e.g. SELECT). Should always be empty."` + GlobalPermissions []string `json:"globalPermissions,omitempty" jsonschema:"Global permissions requested on this data object. Must always be READ."` +} + +// CreateDataAccessRequestInput holds the parameters required to create a new data access request. +type CreateDataAccessRequestInput struct { + Name *string + Description string + UserIDs []string + What []CreateDataAccessRequestWhatInput +} + +// DataAccessRequestSummary is the simplified result of creating an access request. +type DataAccessRequestSummary struct { + ID string `json:"id" jsonschema:"Unique identifier of the created access request"` + Name *string `json:"name,omitempty" jsonschema:"Display name of the access request"` + Description string `json:"description" jsonschema:"Description of the access request"` + Status string `json:"status" jsonschema:"Current status of the access request (e.g. Created, Approval, Implementation, Closed)"` + Outcome string `json:"outcome" jsonschema:"Current outcome of the access request"` + Url string `json:"url" jsonschema:"Url in the Collibra UI to view access request"` +} + +// CreateDataAccessRequest creates a new Data Access request via the SDK's AccessRequestClient. +func CreateDataAccessRequest(ctx context.Context, httpClient *http.Client, input CreateDataAccessRequestInput) (*DataAccessRequestSummary, error) { + collibraHost, ok := chip.GetCollibraHost(ctx) + if !ok { + return nil, fmt.Errorf("collibra host not configured in context") + } + dataAccessURL := strings.TrimSuffix(collibraHost, "/") + "/dataAccess" + + collibraClient, err := sdk.NewClient(dataAccessURL, sdk.WithHTTPClient(httpClient)) + if err != nil { + return nil, fmt.Errorf("failed to create data access client: %w", err) + } + + what := make([]types.AccessRequestWhatInput, 0, len(input.What)) + for _, w := range input.What { + what = append(what, types.AccessRequestWhatInput{ + DataObject: &types.AccessRequestDataObjectWhatInput{ + Id: w.DataObjectID, + Permissions: w.Permissions, + GlobalPermissions: w.GlobalPermissions, + }, + }) + } + + req := types.AccessRequestInput{ + Name: input.Name, + Description: &input.Description, + Who: &types.AccessRequestWhoInput{ + Users: input.UserIDs, + }, + What: what, + } + + ar, err := collibraClient.AccessRequest().CreateAccessRequest(ctx, req) + if err != nil { + return nil, err + } + + requestURL := strings.TrimSuffix(collibraHost, "/") + "/data-access/access-requests/" + ar.Id + + return &DataAccessRequestSummary{ + ID: ar.Id, + Name: ar.Name, + Description: ar.Description, + Status: string(ar.Status), + Outcome: string(ar.Outcome), + Url: requestURL, + }, nil +} + +func mapToDataAccessObject(o *types.DataObject) *DataAccessObject { + out := &DataAccessObject{ + ID: o.Id, + Name: o.Name, + FullName: o.FullName, + Type: o.Type, + DataType: o.DataType, + Deleted: o.Deleted, + Description: o.Description, + } + if o.DataSource != nil { + out.DataSourceID = o.DataSource.Id + } + if len(o.ApplicablePermissions) > 0 { + out.ApplicablePermissions = make([]DataAccessPermission, 0, len(o.ApplicablePermissions)) + for _, p := range o.ApplicablePermissions { + out.ApplicablePermissions = append(out.ApplicablePermissions, DataAccessPermission{ + Name: p.Name, + Description: p.Description, + }) + } + } + return out +} + func mapToDataAccessIdentity(u *types.User) *DataAccessIdentity { return &DataAccessIdentity{ ID: u.Id, diff --git a/pkg/tools/create_data_access_request/tool.go b/pkg/tools/create_data_access_request/tool.go new file mode 100644 index 0000000..b96ba08 --- /dev/null +++ b/pkg/tools/create_data_access_request/tool.go @@ -0,0 +1,131 @@ +package create_data_access_request + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/collibra/chip/pkg/chip" + "github.com/collibra/chip/pkg/clients" + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +// aiDescriptionSuffix is appended to every description so the access request is clearly +// attributed to an AI agent. +const aiDescriptionSuffix = "This access request was created by AI." + +// suggestedNameMaxLen caps the length of a name suggestion derived from the purpose. +const suggestedNameMaxLen = 80 + +// Status values returned in the Output. +const ( + statusNeedsNameConfirmation = "needs_name_confirmation" + statusCreated = "created" +) + +type Input struct { + Name string `json:"name,omitempty" jsonschema:"Optional. Display name of the access request. If omitted, the tool returns a suggested name derived from the purpose and asks the agent to confirm it with the user before retrying."` + Purpose string `json:"purpose" jsonschema:"Required. The user-supplied purpose / business justification for the access request. This is used verbatim as the description of the access request. The tool always appends a note indicating the request was created by AI."` + UserIDs []string `json:"userIds" jsonschema:"Required. IDs of the beneficiary users (the WHO of the request). Resolve these via the search_data_access_identities tool before calling."` + What []clients.CreateDataAccessRequestWhatInput `json:"what" jsonschema:"Required. The data objects the users are requesting access to (the WHAT of the request). Each item references a data object ID and optional requested permissions. Resolve the data object IDs via the search_data_access_objects tool before calling."` +} + +type Output struct { + Status string `json:"status,omitempty" jsonschema:"Outcome of the call: needs_name_confirmation (no name was supplied — confirm the suggestedName with the user and call again with name set), or created (the request was successfully created)."` + Message string `json:"message,omitempty" jsonschema:"Human-readable explanation of the status. When status is needs_name_confirmation, this tells the agent to confirm the suggested name with the user."` + SuggestedName string `json:"suggestedName,omitempty" jsonschema:"Name suggestion derived from the purpose. Present only when status is needs_name_confirmation."` + Request *clients.DataAccessRequestSummary `json:"request,omitempty" jsonschema:"The created access request, if successful."` + Error string `json:"error,omitempty" jsonschema:"Error message if the access request could not be created."` +} + +func NewTool(collibraClient *http.Client) *chip.Tool[Input, Output] { + return &chip.Tool[Input, Output]{ + Name: "create_data_access_request", + Description: "Create a new Collibra Data Access request. Requires the WHO (beneficiary user IDs, obtained via search_data_access_identities), the WHAT (data objects, obtained via search_data_access_objects), and a user-supplied purpose that is used as the description. If no name is supplied, the tool returns a suggested name derived from the purpose with status needs_name_confirmation — confirm the suggestion (or get a replacement) with the user, then call again with name set. The description always ends with a note stating that the request was created by AI.", + Handler: handle(collibraClient), + Permissions: []string{}, + Annotations: &mcp.ToolAnnotations{ReadOnlyHint: false, DestructiveHint: new(false)}, + } +} + +func handle(collibraClient *http.Client) chip.ToolHandlerFunc[Input, Output] { + return func(ctx context.Context, input Input) (Output, error) { + purpose := strings.TrimSpace(input.Purpose) + if purpose == "" { + return Output{Error: "purpose is required — ask the user for the business justification for this access request"}, nil + } + if len(input.UserIDs) == 0 { + return Output{Error: "at least one beneficiary user ID is required — resolve them with search_data_access_identities"}, nil + } + if len(input.What) == 0 { + return Output{Error: "at least one data object is required — resolve them with search_data_access_objects"}, nil + } + for i, w := range input.What { + if strings.TrimSpace(w.DataObjectID) == "" { + return Output{Error: fmt.Sprintf("what[%d].dataObjectId is required", i)}, nil + } + } + + name := strings.TrimSpace(input.Name) + if name == "" { + suggested := suggestNameFromPurpose(purpose) + return Output{ + Status: statusNeedsNameConfirmation, + SuggestedName: suggested, + Message: fmt.Sprintf("No name was supplied. Suggested name based on the purpose: %q. Confirm this with the user (or ask for a different name), then call create_data_access_request again with the confirmed name in the `name` field.", suggested), + }, nil + } + + clientInput := clients.CreateDataAccessRequestInput{ + Name: &name, + Description: buildDescription(purpose), + UserIDs: input.UserIDs, + What: input.What, + } + + req, err := clients.CreateDataAccessRequest(ctx, collibraClient, clientInput) + if err != nil { + return Output{Error: fmt.Sprintf("Failed to create data access request: %s", err.Error())}, nil + } + return Output{Status: statusCreated, Request: req}, nil + } +} + +func buildDescription(purpose string) string { + if strings.Contains(purpose, aiDescriptionSuffix) { + return purpose + } + if !strings.HasSuffix(purpose, ".") { + purpose = purpose + "." + } + return purpose + " " + aiDescriptionSuffix +} + +// suggestNameFromPurpose derives a short, human-readable name from the purpose text. +// It takes the first sentence/line, strips the AI-attribution suffix, collapses whitespace, +// truncates to suggestedNameMaxLen characters at a word boundary, and prefixes it. +func suggestNameFromPurpose(purpose string) string { + summary := strings.ReplaceAll(purpose, aiDescriptionSuffix, "") + summary = strings.Map(func(r rune) rune { + if r == '\n' || r == '\r' || r == '\t' { + return ' ' + } + return r + }, summary) + if idx := strings.IndexAny(summary, ".!?"); idx >= 0 { + summary = summary[:idx] + } + summary = strings.Join(strings.Fields(summary), " ") + if summary == "" { + return "Access request" + } + if len(summary) > suggestedNameMaxLen { + truncated := summary[:suggestedNameMaxLen] + if sp := strings.LastIndex(truncated, " "); sp > suggestedNameMaxLen/2 { + truncated = truncated[:sp] + } + summary = strings.TrimRight(truncated, " ,;:-") + } + return "Access request: " + summary +} diff --git a/pkg/tools/get_data_access_control_details.go b/pkg/tools/get_data_access_control_details/tool.go similarity index 87% rename from pkg/tools/get_data_access_control_details.go rename to pkg/tools/get_data_access_control_details/tool.go index a48b0f1..328d018 100644 --- a/pkg/tools/get_data_access_control_details.go +++ b/pkg/tools/get_data_access_control_details/tool.go @@ -1,4 +1,4 @@ -package tools +package get_data_access_control_details import ( "context" @@ -7,6 +7,7 @@ import ( "github.com/collibra/chip/pkg/chip" "github.com/collibra/chip/pkg/clients" + "github.com/modelcontextprotocol/go-sdk/mcp" ) type DataAccessControlInput struct { @@ -19,12 +20,13 @@ type DataAccessControlOutput struct { Found bool `json:"found" jsonschema:"Whether the data access control was found"` } -func NewGetDataAccessControlDetailsTool(collibraClient *http.Client) *chip.Tool[DataAccessControlInput, DataAccessControlOutput] { +func NewTool(collibraClient *http.Client) *chip.Tool[DataAccessControlInput, DataAccessControlOutput] { return &chip.Tool[DataAccessControlInput, DataAccessControlOutput]{ Name: "get_data_access_control_details", Description: "Retrieve detailed information about a specific Collibra Data Access control by its id. Returns the access control's name, description, state (ACTIVE, INACTIVE, DELETED), action type (GRANT, MASK, FILTER, SHARE, GROUP, FILTERRULE), grant category, policy rule, external management status, ABAC scope parse status, and timestamps. Use this to inspect an individual access control when you know its ID.", Handler: handleGetDataAccessControlDetails(collibraClient), Permissions: []string{}, + Annotations: &mcp.ToolAnnotations{ReadOnlyHint: true, DestructiveHint: new(false)}, } } diff --git a/pkg/tools/register.go b/pkg/tools/register.go index 6845198..126764b 100644 --- a/pkg/tools/register.go +++ b/pkg/tools/register.go @@ -7,11 +7,13 @@ import ( "github.com/collibra/chip/pkg/tools/add_business_term" "github.com/collibra/chip/pkg/tools/add_data_classification_match" "github.com/collibra/chip/pkg/tools/create_asset" + "github.com/collibra/chip/pkg/tools/create_data_access_request" "github.com/collibra/chip/pkg/tools/discover_business_glossary" "github.com/collibra/chip/pkg/tools/discover_data_assets" "github.com/collibra/chip/pkg/tools/get_asset_details" "github.com/collibra/chip/pkg/tools/get_business_term_data" "github.com/collibra/chip/pkg/tools/get_column_semantics" + "github.com/collibra/chip/pkg/tools/get_data_access_control_details" "github.com/collibra/chip/pkg/tools/get_lineage_downstream" "github.com/collibra/chip/pkg/tools/get_lineage_entity" "github.com/collibra/chip/pkg/tools/get_lineage_transformation" @@ -20,14 +22,16 @@ import ( "github.com/collibra/chip/pkg/tools/get_table_semantics" "github.com/collibra/chip/pkg/tools/list_asset_types" "github.com/collibra/chip/pkg/tools/list_data_contracts" - "github.com/collibra/chip/pkg/tools/prepare_create_asset" "github.com/collibra/chip/pkg/tools/prepare_add_business_term" + "github.com/collibra/chip/pkg/tools/prepare_create_asset" "github.com/collibra/chip/pkg/tools/pull_data_contract_manifest" "github.com/collibra/chip/pkg/tools/push_data_contract_manifest" "github.com/collibra/chip/pkg/tools/remove_data_classification_match" "github.com/collibra/chip/pkg/tools/search_asset_keyword" - "github.com/collibra/chip/pkg/tools/search_data_classification_matches" + "github.com/collibra/chip/pkg/tools/search_data_access_identities" + "github.com/collibra/chip/pkg/tools/search_data_access_objects" "github.com/collibra/chip/pkg/tools/search_data_classes" + "github.com/collibra/chip/pkg/tools/search_data_classification_matches" "github.com/collibra/chip/pkg/tools/search_lineage_entities" "github.com/collibra/chip/pkg/tools/search_lineage_transformations" ) @@ -56,17 +60,21 @@ func RegisterAll(server *chip.Server, client *http.Client, toolConfig *chip.Serv toolRegister(server, toolConfig, prepare_add_business_term.NewTool(client)) toolRegister(server, toolConfig, get_business_term_data.NewTool(client)) toolRegister(server, toolConfig, get_column_semantics.NewTool(client)) + toolRegister(server, toolConfig, get_data_access_control_details.NewTool(client)) toolRegister(server, toolConfig, get_lineage_downstream.NewTool(client)) toolRegister(server, toolConfig, get_lineage_entity.NewTool(client)) toolRegister(server, toolConfig, get_lineage_transformation.NewTool(client)) toolRegister(server, toolConfig, get_lineage_upstream.NewTool(client)) toolRegister(server, toolConfig, get_measure_data.NewTool(client)) toolRegister(server, toolConfig, get_table_semantics.NewTool(client)) + toolRegister(server, toolConfig, search_data_access_identities.NewTool(client)) + toolRegister(server, toolConfig, search_data_access_objects.NewTool(client)) toolRegister(server, toolConfig, search_lineage_entities.NewTool(client)) toolRegister(server, toolConfig, search_lineage_transformations.NewTool(client)) toolRegister(server, toolConfig, prepare_create_asset.NewTool(client)) toolRegister(server, toolConfig, add_business_term.NewTool(client)) toolRegister(server, toolConfig, create_asset.NewTool(client)) + toolRegister(server, toolConfig, create_data_access_request.NewTool(client)) } func toolRegister[In, Out any](server *chip.Server, toolConfig *chip.ServerToolConfig, tool *chip.Tool[In, Out]) { diff --git a/pkg/tools/search_data_access_controls.go b/pkg/tools/search_data_access_controls.go deleted file mode 100644 index 53eca41..0000000 --- a/pkg/tools/search_data_access_controls.go +++ /dev/null @@ -1,81 +0,0 @@ -package tools - -import ( - "context" - "fmt" - "net/http" - - "github.com/collibra/chip/pkg/chip" - "github.com/collibra/chip/pkg/clients" -) - -type SearchDataAccessControlsInput struct { - Name string `json:"name,omitempty" jsonschema:"Optional. Filter by name (case-insensitive contains match)."` - Actions []string `json:"actions,omitempty" jsonschema:"Optional. Filter by one or more action types. Valid values: Grant, Mask, Filter, Share, Group, FilterRule."` - States []string `json:"states,omitempty" jsonschema:"Optional. Filter by one or more states. Valid values: Active, Inactive, Deleted."` - Cursor string `json:"cursor,omitempty" jsonschema:"Optional. Cursor from a previous response to fetch the next page of results."` - PageSize int `json:"pageSize,omitempty" jsonschema:"Optional. Number of results per page (default: 25, max: 25)."` -} - -type SearchDataAccessRolesInput struct { - Name string `json:"name,omitempty" jsonschema:"Optional. Filter by name (case-insensitive contains match)."` - States []string `json:"states,omitempty" jsonschema:"Optional. Filter by one or more states. Valid values: Active, Inactive, Deleted."` - Cursor string `json:"cursor,omitempty" jsonschema:"Optional. Cursor from a previous response to fetch the next page of results."` - PageSize int `json:"pageSize,omitempty" jsonschema:"Optional. Number of results per page (default: 25, max: 25)."` -} - -type SearchDataAccessControlsOutput struct { - Results []*clients.DataAccessControlDetails `json:"results" jsonschema:"The matching data access controls."` - NextCursor *string `json:"nextCursor,omitempty" jsonschema:"Cursor to pass in the next request to fetch the following page. Absent when there are no more results."` - Error string `json:"error,omitempty" jsonschema:"Error message if the search could not be completed."` -} - -func NewSearchDataAccessControlsTool(collibraClient *http.Client) *chip.Tool[SearchDataAccessControlsInput, SearchDataAccessControlsOutput] { - return &chip.Tool[SearchDataAccessControlsInput, SearchDataAccessControlsOutput]{ - Name: "search_data_access_controls", - Description: "Search for data access controls in Collibra Data Access. Results can be filtered by name (case-insensitive contains), action type (Grant, Mask, Filter, Share, Group, FilterRule), and/or state (Active, Inactive, Deleted). All filters are optional and can be combined. Returns a paginated list — use the returned cursor to fetch subsequent pages.", - Handler: handleSearchDataAccessControls(collibraClient), - Permissions: []string{}, - } -} - -func handleSearchDataAccessControls(collibraClient *http.Client) chip.ToolHandlerFunc[SearchDataAccessControlsInput, SearchDataAccessControlsOutput] { - return func(ctx context.Context, input SearchDataAccessControlsInput) (SearchDataAccessControlsOutput, error) { - result, err := clients.SearchDataAccessControls(ctx, collibraClient, input.Name, input.Actions, input.States, input.Cursor, input.PageSize) - if err != nil { - return SearchDataAccessControlsOutput{ - Error: fmt.Sprintf("Failed to search data access controls: %s", err.Error()), - }, nil - } - - return SearchDataAccessControlsOutput{ - Results: result.Items, - NextCursor: result.NextCursor, - }, nil - } -} - -func NewSearchDataAccessRolesTool(collibraClient *http.Client) *chip.Tool[SearchDataAccessRolesInput, SearchDataAccessControlsOutput] { - return &chip.Tool[SearchDataAccessRolesInput, SearchDataAccessControlsOutput]{ - Name: "search_data_access_roles", - Description: "Search for data access roles (Grant-type access controls) in Collibra Data Access. Results can be filtered by name (case-insensitive contains) and/or state (Active, Inactive, Deleted). All filters are optional and can be combined. Returns a paginated list — use the returned cursor to fetch subsequent pages.", - Handler: handleSearchDataAccessRoles(collibraClient), - Permissions: []string{}, - } -} - -func handleSearchDataAccessRoles(collibraClient *http.Client) chip.ToolHandlerFunc[SearchDataAccessRolesInput, SearchDataAccessControlsOutput] { - return func(ctx context.Context, input SearchDataAccessRolesInput) (SearchDataAccessControlsOutput, error) { - result, err := clients.SearchDataAccessControls(ctx, collibraClient, input.Name, []string{"Grant"}, input.States, input.Cursor, input.PageSize) - if err != nil { - return SearchDataAccessControlsOutput{ - Error: fmt.Sprintf("Failed to search data access roles: %s", err.Error()), - }, nil - } - - return SearchDataAccessControlsOutput{ - Results: result.Items, - NextCursor: result.NextCursor, - }, nil - } -} diff --git a/pkg/tools/search_data_access_identities.go b/pkg/tools/search_data_access_identities/tool.go similarity index 50% rename from pkg/tools/search_data_access_identities.go rename to pkg/tools/search_data_access_identities/tool.go index ad0fc64..e1cd8d4 100644 --- a/pkg/tools/search_data_access_identities.go +++ b/pkg/tools/search_data_access_identities/tool.go @@ -1,4 +1,4 @@ -package tools +package search_data_access_identities import ( "context" @@ -7,33 +7,33 @@ import ( "github.com/collibra/chip/pkg/chip" "github.com/collibra/chip/pkg/clients" + "github.com/modelcontextprotocol/go-sdk/mcp" ) type SearchDataAccessIdentitiesInput struct { Email string `json:"email,omitempty" jsonschema:"Optional. Exact email address to look up the user by."` - Name string `json:"name,omitempty" jsonschema:"Optional. Search string for a case-insensitive contains match on the user's display name. When used without email, SearchUsers is called server-side. When used with email, it is applied as a client-side filter on the result."` - Cursor string `json:"cursor,omitempty" jsonschema:"Optional. Cursor from a previous response to fetch the next page of results. Only applicable for name-based searches."` - PageSize int `json:"pageSize,omitempty" jsonschema:"Optional. Number of results per page (default: 25, max: 25). Only applicable for name-based searches."` + Name string `json:"name,omitempty" jsonschema:"Optional. Search string for a case-insensitive contains match on the user's display name. When used without email, ListUsers is called server-side. When used with email, it is applied as a client-side filter on the result."` + PageSize int `json:"pageSize,omitempty" jsonschema:"Optional. Maximum number of results to return (default: 25, max: 25). Only applicable for name-based searches."` } type SearchDataAccessIdentitiesOutput struct { - Results []*clients.DataAccessIdentity `json:"results" jsonschema:"The matching Data Access users."` - NextCursor *string `json:"nextCursor,omitempty" jsonschema:"Cursor to pass in the next request to fetch the following page. Only present for name-based searches with more results available."` - Error string `json:"error,omitempty" jsonschema:"Error message if the search could not be completed."` + Results []*clients.DataAccessIdentity `json:"results" jsonschema:"The matching Data Access users."` + Error string `json:"error,omitempty" jsonschema:"Error message if the search could not be completed."` } -func NewSearchDataAccessIdentitiesTool(collibraClient *http.Client) *chip.Tool[SearchDataAccessIdentitiesInput, SearchDataAccessIdentitiesOutput] { +func NewTool(collibraClient *http.Client) *chip.Tool[SearchDataAccessIdentitiesInput, SearchDataAccessIdentitiesOutput] { return &chip.Tool[SearchDataAccessIdentitiesInput, SearchDataAccessIdentitiesOutput]{ Name: "search_data_access_identities", - Description: "Search for Data Access users (identities) by name and/or email. Providing email performs an exact lookup; providing name performs a case-insensitive contains search via SearchUsers. Both can be combined: email resolves the user, name filters the result client-side. Name-only searches are paginated — use the returned cursor to fetch subsequent pages.", + Description: "Search for Data Access users (identities) by name and/or email. Providing email performs an exact lookup; providing name performs a case-insensitive contains search via ListUsers. Both can be combined: email resolves the user, name filters the result client-side.", Handler: handleSearchDataAccessIdentities(collibraClient), Permissions: []string{}, + Annotations: &mcp.ToolAnnotations{ReadOnlyHint: true, DestructiveHint: new(false)}, } } func handleSearchDataAccessIdentities(collibraClient *http.Client) chip.ToolHandlerFunc[SearchDataAccessIdentitiesInput, SearchDataAccessIdentitiesOutput] { return func(ctx context.Context, input SearchDataAccessIdentitiesInput) (SearchDataAccessIdentitiesOutput, error) { - result, err := clients.SearchDataAccessIdentities(ctx, collibraClient, input.Name, input.Email, input.Cursor, input.PageSize) + result, err := clients.SearchDataAccessIdentities(ctx, collibraClient, input.Name, input.Email, input.PageSize) if err != nil { return SearchDataAccessIdentitiesOutput{ Error: fmt.Sprintf("Failed to search Data Access identities: %s", err.Error()), @@ -41,8 +41,7 @@ func handleSearchDataAccessIdentities(collibraClient *http.Client) chip.ToolHand } return SearchDataAccessIdentitiesOutput{ - Results: result.Items, - NextCursor: result.NextCursor, + Results: result.Items, }, nil } } diff --git a/pkg/tools/search_data_access_objects/tool.go b/pkg/tools/search_data_access_objects/tool.go new file mode 100644 index 0000000..cf79e1a --- /dev/null +++ b/pkg/tools/search_data_access_objects/tool.go @@ -0,0 +1,51 @@ +package search_data_access_objects + +import ( + "context" + "fmt" + "net/http" + + "github.com/collibra/chip/pkg/chip" + "github.com/collibra/chip/pkg/clients" + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +type SearchDataAccessObjectsInput struct { + Name string `json:"name,omitempty" jsonschema:"Optional. Filter by name (case-insensitive contains match on data object name)."` + DataSources []string `json:"dataSources,omitempty" jsonschema:"Optional. Restrict to data objects belonging to one or more data sources (data source IDs)."` + Types []string `json:"types,omitempty" jsonschema:"Optional. Restrict to data objects of one or more types (e.g. table, column, schema, view)."` + Parents []string `json:"parents,omitempty" jsonschema:"Optional. Restrict to data objects whose direct parent matches one of the given data object IDs."` + Ancestors []string `json:"ancestors,omitempty" jsonschema:"Optional. Restrict to data objects whose ancestors include one of the given data object IDs."` + IncludeDeleted bool `json:"includeDeleted,omitempty" jsonschema:"Optional. If true, also includes data objects that no longer exist in the source. Defaults to false."` + PageSize int `json:"pageSize,omitempty" jsonschema:"Optional. Maximum number of results to return (default: 25, max: 25)."` +} + +type SearchDataAccessObjectsOutput struct { + Results []*clients.DataAccessObject `json:"results" jsonschema:"The matching data objects."` + Error string `json:"error,omitempty" jsonschema:"Error message if the search could not be completed."` +} + +func NewTool(collibraClient *http.Client) *chip.Tool[SearchDataAccessObjectsInput, SearchDataAccessObjectsOutput] { + return &chip.Tool[SearchDataAccessObjectsInput, SearchDataAccessObjectsOutput]{ + Name: "search_data_access_objects", + Description: "Search for data objects in Collibra Data Access. Data objects represent tables, columns, schemas, views, and other entities tracked in registered data sources. Filters can be combined: name (case-insensitive contains), dataSources (data source IDs), types (e.g. table, column), parents/ancestors (other data object IDs), and includeDeleted. Returns up to pageSize matches (default 25, max 25). Each result also includes its applicablePermissions — the source-system permissions (with name and description) that can be requested on the object.", + Handler: handleSearchDataAccessObjects(collibraClient), + Permissions: []string{}, + Annotations: &mcp.ToolAnnotations{ReadOnlyHint: true, DestructiveHint: new(false)}, + } +} + +func handleSearchDataAccessObjects(collibraClient *http.Client) chip.ToolHandlerFunc[SearchDataAccessObjectsInput, SearchDataAccessObjectsOutput] { + return func(ctx context.Context, input SearchDataAccessObjectsInput) (SearchDataAccessObjectsOutput, error) { + result, err := clients.SearchDataAccessObjects(ctx, collibraClient, input.Name, input.DataSources, input.Types, input.Parents, input.Ancestors, input.IncludeDeleted, input.PageSize) + if err != nil { + return SearchDataAccessObjectsOutput{ + Error: fmt.Sprintf("Failed to search data access objects: %s", err.Error()), + }, nil + } + + return SearchDataAccessObjectsOutput{ + Results: result.Items, + }, nil + } +} From 29850f23c0c0f1b9e0d1143b1a7815044d1b2278 Mon Sep 17 00:00:00 2001 From: Wouter Cordewiner <73988312+wouterc-collibra@users.noreply.github.com> Date: Mon, 18 May 2026 09:27:57 +0200 Subject: [PATCH 06/13] github.com/collibra/data-access-go-sdk v0.0.61 --- go.mod | 4 +--- go.sum | 2 ++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index e2e5ce4..50e8147 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/collibra/chip go 1.26.2 require ( - github.com/collibra/data-access-go-sdk v1.0.0 + github.com/collibra/data-access-go-sdk v0.0.61 github.com/google/go-querystring v1.1.0 github.com/google/jsonschema-go v0.4.2 github.com/google/uuid v1.6.0 @@ -12,8 +12,6 @@ require ( github.com/spf13/viper v1.21.0 ) -replace github.com/collibra/data-access-go-sdk => /Users/wouterc/w/data-access-go-sdk-mcp - require ( github.com/Khan/genqlient v0.8.2-0.20251028054717-8ddeeee0a15a // indirect github.com/Masterminds/semver/v3 v3.5.0 // indirect diff --git a/go.sum b/go.sum index 7d659b8..2d23b5d 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAw github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/collibra/data-access-go-sdk v0.0.61 h1:Swmvmx279BAmJfTBeaeBQDP3yDazhr5q8p0V3pN6G9M= +github.com/collibra/data-access-go-sdk v0.0.61/go.mod h1:JPsGzZNdbTekWeNifho8xHbFKdyw8G5LxECtUZxYyYI= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= From 88248da9436a99156d6addc63e485f43d3249c0e Mon Sep 17 00:00:00 2001 From: Wouter Cordewiner <73988312+wouterc-collibra@users.noreply.github.com> Date: Wed, 22 Jul 2026 09:14:54 +0200 Subject: [PATCH 07/13] Add data access skills --- README.md | 5 + SKILLS.md | 13 +- go.mod | 8 +- go.sum | 20 +- pkg/clients/data_access_client.go | 257 +++++++++++++++++- .../files/collibra/data-access/SKILL.md | 48 ++++ .../check_user_data_object_access/tool.go | 63 +++++ .../tool_test.go | 155 +++++++++++ pkg/tools/register.go | 12 +- 9 files changed, 549 insertions(+), 32 deletions(-) create mode 100644 pkg/skills/files/collibra/data-access/SKILL.md create mode 100644 pkg/tools/check_user_data_object_access/tool.go create mode 100644 pkg/tools/check_user_data_object_access/tool_test.go diff --git a/README.md b/README.md index a513b33..46b5a38 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ This Go-based MCP server acts as a bridge between AI applications and Collibra, ### Read Tools +- [`check_user_data_object_access`](pkg/tools/check_user_data_object_access/) - Check whether a user has access to one or more data objects (by ID) and through which roles (access controls). Defaults to the current user +- [`get_data_access_data_source`](pkg/tools/get_data_access_data_source/) - Fetch a Collibra Data Access data source by ID, resolving an opaque data source ID to its name, type, and description - [`discover_business_glossary`](pkg/tools/discover_business_glossary/) - Ask questions about terms and definitions. **Requires:** `dgc.ai-copilot` - [`discover_data_assets`](pkg/tools/discover_data_assets/) - Query available data assets using natural language. **Requires:** `dgc.ai-copilot` - [`get_asset_details`](pkg/tools/get_asset_details/) - Retrieve detailed information about specific assets by UUID @@ -26,6 +28,8 @@ This Go-based MCP server acts as a bridge between AI applications and Collibra, - [`prepare_create_asset`](pkg/tools/prepare_create_asset/) - Read-only companion to `create_asset`: enumerate available asset types and domains, resolve a UUID/publicId/displayName for either, and hydrate the scoped attribute and relation schema for a chosen pair - [`pull_data_contract_manifest`](pkg/tools/pull_data_contract_manifest/) - Download manifest for a data contract - [`search_asset_keyword`](pkg/tools/search_asset_keyword/) - Wildcard keyword search for assets +- [`search_data_access_identities`](pkg/tools/search_lineage_transformations/) - Search for Data Access users (identities) by name and/or email +- [`search_data_access_objects`](pkg/tools/search_lineage_transformations/) - Search for data objects in Collibra Data Access (tables, columns, schemas, views, and other entities tracked in registered data sources) - [`search_data_class`](pkg/tools/search_data_classes/) - Search for data classes with filters. **Requires:** `dgc.data-classes-read` - [`search_data_classification_match`](pkg/tools/search_data_classification_matches/) - Search for associations between data classes and assets. **Requires:** `dgc.classify`, `dgc.catalog` - [`search_lineage_entities`](pkg/tools/search_lineage_entities/) - Search for entities in the technical lineage graph @@ -35,6 +39,7 @@ This Go-based MCP server acts as a bridge between AI applications and Collibra, - [`add_data_classification_match`](pkg/tools/add_data_classification_match/) - Associate a data class with an asset. **Requires:** `dgc.classify`, `dgc.catalog` - [`create_asset`](pkg/tools/create_asset/) - Create a new asset of any type. Resolves `assetType` (UUID, publicId, or display name), `domain` (UUID or name), `status` (UUID or name), and attributes (by name or typeId) server-side; converts Markdown to HTML for `RICH_TEXT` attributes; gates on duplicate-name (default `allowDuplicate: false`) +- [`create_data_access_request`](pkg/tools/create_asset/) - Create a new Collibra Data Access request on behalf of one or more users for one or more data objects - [`edit_asset`](pkg/tools/edit_asset/) - Edit an existing asset via a list of typed operations: - `update_attribute`, `add_attribute`, `remove_attribute` - change, append, or clear an attribute value (e.g. `Definition`, `Note`) - `update_property` - rename the asset (`name`), change its `displayName`, or change its `statusId` (status name or UUID accepted) diff --git a/SKILLS.md b/SKILLS.md index 2c9e872..cf16fa3 100644 --- a/SKILLS.md +++ b/SKILLS.md @@ -10,13 +10,14 @@ those tools. Skill content lives in [`pkg/skills/files/collibra/`](pkg/skills/fi ## Current skills -| Name | Topic | -|---|---| -| `collibra/index` | Navigator — start here when unsure which skill applies | -| `collibra/discovery` | Semantic vs keyword search; resolving names to UUIDs | -| `collibra/lineage` | Technical lineage; DGC UUID ↔ lineage entity ID bridge; column-level workaround | +| Name | Topic | +|-------------------------|---| +| `collibra/index` | Navigator — start here when unsure which skill applies | +| `collibra/discovery` | Semantic vs keyword search; resolving names to UUIDs | +| `collibra/lineage` | Technical lineage; DGC UUID ↔ lineage entity ID bridge; column-level workaround | | `collibra/asset-create` | `create_asset` workflow; RICH_TEXT Markdown handling; duplicate gating | -| `collibra/asset-edit` | `edit_asset` operation types | +| `collibra/asset-edit` | `edit_asset` operation types | +| `collibra/data-access` | Manages who can access what data, through grants, masks and filters | Each skill is one `SKILL.md` per directory, with frontmatter (`description`, `related`) and an optional `references/` directory for bundled reference documents. diff --git a/go.mod b/go.mod index c2f6e25..0296f05 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,9 @@ module github.com/collibra/chip -go 1.26.2 +go 1.26.5 require ( - github.com/collibra/data-access-go-sdk v0.0.61 + github.com/collibra/data-access-go-sdk v0.0.69 github.com/google/go-querystring v1.1.0 github.com/google/jsonschema-go v0.4.2 github.com/google/uuid v1.6.0 @@ -14,7 +14,7 @@ require ( ) require ( - github.com/Khan/genqlient v0.8.2-0.20251028054717-8ddeeee0a15a // indirect + github.com/Khan/genqlient v0.8.2-0.20260527022710-6bbde3684dd6 // indirect github.com/Masterminds/semver/v3 v3.5.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect @@ -28,7 +28,7 @@ require ( github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect - github.com/vektah/gqlparser/v2 v2.5.30 // indirect + github.com/vektah/gqlparser/v2 v2.5.36 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/oauth2 v0.35.0 // indirect diff --git a/go.sum b/go.sum index 1cc84be..09290c4 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,9 @@ -github.com/Khan/genqlient v0.8.2-0.20251028054717-8ddeeee0a15a h1:kx/iDWW6lRgNBqTL1z6UB7ajcZR3OcVLKVJ39QkUnUw= -github.com/Khan/genqlient v0.8.2-0.20251028054717-8ddeeee0a15a/go.mod h1:guUHwMi8ByjIvs3TyAPM+V9ryaW305CtK7+aCeP2Jzc= +github.com/Khan/genqlient v0.8.2-0.20260527022710-6bbde3684dd6 h1:Y+TbviEJfdsIS2jwgSLkSK2t0O1eVaRsvtW5oPQ9it4= +github.com/Khan/genqlient v0.8.2-0.20260527022710-6bbde3684dd6/go.mod h1:guUHwMi8ByjIvs3TyAPM+V9ryaW305CtK7+aCeP2Jzc= github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= -github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= -github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= -github.com/collibra/data-access-go-sdk v0.0.61 h1:Swmvmx279BAmJfTBeaeBQDP3yDazhr5q8p0V3pN6G9M= -github.com/collibra/data-access-go-sdk v0.0.61/go.mod h1:JPsGzZNdbTekWeNifho8xHbFKdyw8G5LxECtUZxYyYI= +github.com/collibra/data-access-go-sdk v0.0.69 h1:106+SRdPkWGNhIkSwsyJZJ+ttkC+HT1lQrquo/Qgdss= +github.com/collibra/data-access-go-sdk v0.0.69/go.mod h1:oT1RYAOH5ZVZzjo4ut96QjtZLshqBXIieeVOstVyRTw= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= @@ -55,8 +53,6 @@ github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc= github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg= github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0= github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0= -github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= -github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= @@ -69,8 +65,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/vektah/gqlparser/v2 v2.5.30 h1:EqLwGAFLIzt1wpx1IPpY67DwUujF1OfzgEyDsLrN6kE= -github.com/vektah/gqlparser/v2 v2.5.30/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo= +github.com/vektah/gqlparser/v2 v2.5.36 h1:CN9mKVHgMkc+XftdOWIhb4HEL8wKSYkFAqhf8booa7s= +github.com/vektah/gqlparser/v2 v2.5.36/go.mod h1:cAJ9qwVgPaUkWv6Gn8vn0mqOE0Ui5Pn56wNy5396XWo= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE= @@ -83,8 +79,8 @@ golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= -golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= -golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= +golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= +golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/pkg/clients/data_access_client.go b/pkg/clients/data_access_client.go index 1cb02a8..b515fa4 100644 --- a/pkg/clients/data_access_client.go +++ b/pkg/clients/data_access_client.go @@ -34,6 +34,7 @@ type DataAccessControlDetails struct { What []DataAccessWhatItem `json:"what" jsonschema:"List of access controls that this control applies to (the WHAT scope)"` Who []DataAccessWhoItem `json:"who" jsonschema:"List of principals (users, access controls, data sources) that are granted access by this control"` SyncData []DataAccessSyncData `json:"syncData" jsonschema:"Synchronization status per linked data source. Valid sync statuses: Notconnected, Failed, Outofdate, Inprogress, Synced, Outofsync."` + Url string `json:"url" jsonschema:"Url in the Collibra UI to view access control"` } // DataAccessSyncData holds the sync status of an access control for a single data source. @@ -98,7 +99,7 @@ func GetDataAccessControl(ctx context.Context, httpClient *http.Client, id strin return nil, err } - details := mapToDataAccessControlDetails(ac) + details := mapToDataAccessControlDetails(ctx, ac) for whatItem, err := range accessControlClient.GetAccessControlWhatAccessControlList(ctx, id) { if err != nil { @@ -169,12 +170,65 @@ func mapToDataAccessWhoItem(w *types.AccessWhoItem) DataAccessWhoItem { return item } +// DataAccessDataSource holds the details of a single Data Access data source. +type DataAccessDataSource struct { + ID string `json:"id" jsonschema:"Unique identifier of the data source"` + Name string `json:"name" jsonschema:"Display name of the data source"` + Type string `json:"type" jsonschema:"Type identifier of the data source, set by the connector during a sync (e.g. snowflake, databricks, bigquery)"` + Description string `json:"description" jsonschema:"Description of the data source"` + ParentID string `json:"parentId,omitempty" jsonschema:"Identifier of the parent data source, when this data source has one"` + CreatedAt time.Time `json:"createdAt" jsonschema:"Timestamp when the data source was created"` + ModifiedAt time.Time `json:"modifiedAt" jsonschema:"Timestamp when the data source was last modified"` + Url string `json:"url" jsonschema:"Url in the Collibra UI to view data source"` +} + +// GetDataAccessDataSource retrieves a single Data Access data source by ID. +// It creates an sdk.CollibraClient using chip's existing HTTP client via sdk.WithHTTPClient, +// so URL routing and authentication are handled by chip's RoundTripper. +func GetDataAccessDataSource(ctx context.Context, httpClient *http.Client, id string) (*DataAccessDataSource, error) { + collibraHost, ok := chip.GetCollibraHost(ctx) + if !ok { + return nil, fmt.Errorf("collibra host not configured in context") + } + dataAccessURL := strings.TrimSuffix(collibraHost, "/") + "/dataAccess" + + collibraClient, err := sdk.NewClient(dataAccessURL, sdk.WithHTTPClient(httpClient)) + if err != nil { + return nil, fmt.Errorf("failed to create data access client: %w", err) + } + + ds, err := collibraClient.DataSource().GetDataSource(ctx, id) + if err != nil { + return nil, err + } + + return mapToDataAccessDataSource(ctx, ds), nil +} + +func mapToDataAccessDataSource(ctx context.Context, ds *types.DataSource) *DataAccessDataSource { + uiURL := buildUiUrl(ctx, "data-sources", ds.Id) + out := &DataAccessDataSource{ + ID: ds.Id, + Name: ds.Name, + Type: ds.Type, + Description: ds.Description, + CreatedAt: ds.CreatedAt, + ModifiedAt: ds.ModifiedAt, + Url: uiURL, + } + if ds.Parent != nil { + out.ParentID = ds.Parent.Id + } + return out +} + // DataAccessIdentity represents a user in Collibra Data Access. type DataAccessIdentity struct { ID string `json:"id" jsonschema:"Unique identifier of the user"` Name string `json:"name" jsonschema:"Display name of the user"` Email *string `json:"email,omitempty" jsonschema:"Email address of the user"` Type string `json:"type" jsonschema:"User type: Human or Machine"` + Url string `json:"url" jsonschema:"Url in the Collibra UI to view identity"` } // SearchDataAccessIdentitiesResult holds a page of identities. @@ -209,7 +263,7 @@ func SearchDataAccessIdentities(ctx context.Context, httpClient *http.Client, na return nil, err } - identity := mapToDataAccessIdentity(user) + identity := mapToDataAccessIdentity(ctx, user) if name != "" && !strings.Contains(strings.ToLower(identity.Name), strings.ToLower(name)) { return &SearchDataAccessIdentitiesResult{Items: []*DataAccessIdentity{}}, nil } @@ -233,7 +287,7 @@ func SearchDataAccessIdentities(ctx context.Context, httpClient *http.Client, na if iterErr != nil { return nil, iterErr } - result.Items = append(result.Items, mapToDataAccessIdentity(user)) + result.Items = append(result.Items, mapToDataAccessIdentity(ctx, user)) if len(result.Items) >= limit { break } @@ -252,6 +306,7 @@ type DataAccessObject struct { Description string `json:"description" jsonschema:"Description of the data object"` DataSourceID string `json:"dataSourceId,omitempty" jsonschema:"Identifier of the data source the object belongs to"` ApplicablePermissions []DataAccessPermission `json:"applicablePermissions,omitempty" jsonschema:"Source-system permissions that can be requested or granted on this data object (and its descendants). Each permission carries its name and description."` + Url string `json:"url" jsonschema:"Url in the Collibra UI to view the data object"` } // DataAccessPermission is a permission that can be set on a data object. @@ -312,7 +367,7 @@ func SearchDataAccessObjects(ctx context.Context, httpClient *http.Client, name if iterErr != nil { return nil, iterErr } - result.Items = append(result.Items, mapToDataAccessObject(obj)) + result.Items = append(result.Items, mapToDataAccessObject(ctx, obj)) if len(result.Items) >= limit { break } @@ -320,6 +375,172 @@ func SearchDataAccessObjects(ctx context.Context, httpClient *http.Client, name return result, nil } +// DataObjectAccessRole is an access control (a "role") that grants a user access to a data +// object. It is the trimmed form surfaced in NearestAccessControls. +type DataObjectAccessRole struct { + ID string `json:"id" jsonschema:"Unique identifier of the access control granting the access"` + Name string `json:"name" jsonschema:"Name of the access control (role) granting the access"` + Action string `json:"action" jsonschema:"Action type of the access control: Grant, Mask, Filter, Share, Group, or FilterRule"` + State string `json:"state" jsonschema:"State of the access control: Active, Inactive, or Deleted"` + Category *DataAccessGrantCategory `json:"category,omitempty" jsonschema:"Grant category details, present only for Grant action type"` +} + +// UserDataObjectAccess describes the access a single user has on a single data object, and the +// roles (access controls) that grant it. +type UserDataObjectAccess struct { + HasAccess bool `json:"hasAccess" jsonschema:"Whether the user has any access to the data object"` + Permissions []string `json:"permissions,omitempty" jsonschema:"Source-system permissions the user has on the data object (e.g. SELECT)"` + GlobalPermissions []string `json:"globalPermissions,omitempty" jsonschema:"Global permissions the user has on the data object (e.g. READ)"` + ExpiresAt *time.Time `json:"expiresAt,omitempty" jsonschema:"When the access expires. Only populated when access is granted through a single access control; nil when multiple roles grant access."` + Roles []DataObjectAccessRole `json:"roles" jsonschema:"The access controls (roles) that grant the user access to the data object"` +} + +// ObjectAccessResult ties a resolved data object to the user's access on it. +type ObjectAccessResult struct { + DataObject *DataAccessObject `json:"dataObject" jsonschema:"The resolved data object"` + Access *UserDataObjectAccess `json:"access" jsonschema:"The user's access to the data object, including the granting roles"` +} + +// UnresolvedObjectID is a requested data object ID that could not be resolved to a data object. +type UnresolvedObjectID struct { + ID string `json:"id" jsonschema:"The supplied data object ID that could not be resolved"` + Reason string `json:"reason" jsonschema:"Why it could not be resolved: not_found (no data object exists with this ID)"` +} + +// CheckUserDataObjectAccessResult is the result of checking a user's access to one or more data +// objects identified by ID. +type CheckUserDataObjectAccessResult struct { + User *DataAccessIdentity `json:"user" jsonschema:"The user the access was checked for (the current user when no userId/email was supplied)"` + Results []*ObjectAccessResult `json:"results" jsonschema:"Per-object access results for the IDs that resolved to a data object"` + Unresolved []*UnresolvedObjectID `json:"unresolved,omitempty" jsonschema:"IDs that did not resolve to a data object. Ask the user to correct or drop these."` +} + +// CheckUserDataObjectAccess looks up each supplied data object ID and reports the access the user +// has on it, including the access controls (roles) that grant the access. +// +// The user is resolved as: userID if set, otherwise the user with email, otherwise the current +// user. IDs that do not correspond to an existing data object are returned in Unresolved so the +// caller can ask the user to correct or drop them. Resolve names to IDs via search_data_access_objects. +func CheckUserDataObjectAccess(ctx context.Context, httpClient *http.Client, objectIDs []string, userID, email string) (*CheckUserDataObjectAccessResult, error) { + collibraHost, ok := chip.GetCollibraHost(ctx) + if !ok { + return nil, fmt.Errorf("collibra host not configured in context") + } + dataAccessURL := strings.TrimSuffix(collibraHost, "/") + "/dataAccess" + + collibraClient, err := sdk.NewClient(dataAccessURL, sdk.WithHTTPClient(httpClient)) + if err != nil { + return nil, fmt.Errorf("failed to create data access client: %w", err) + } + + user, err := resolveDataAccessUser(ctx, collibraClient, userID, email) + if err != nil { + return nil, err + } + + result := &CheckUserDataObjectAccessResult{ + User: mapToDataAccessIdentity(ctx, user), + Results: []*ObjectAccessResult{}, + Unresolved: []*UnresolvedObjectID{}, + } + + for _, id := range objectIDs { + trimmed := strings.TrimSpace(id) + if trimmed == "" { + continue + } + + obj, err := collibraClient.DataObject().GetDataObject(ctx, trimmed) + if err != nil { + return nil, fmt.Errorf("failed to fetch data object %q: %w", trimmed, err) + } + if obj == nil || obj.Id == "" { + result.Unresolved = append(result.Unresolved, &UnresolvedObjectID{ + ID: trimmed, + Reason: "not_found", + }) + continue + } + + item, err := collibraClient.DataObject().GetUserAccessToDataObject(ctx, obj.Id, user.Id) + if err != nil { + return nil, fmt.Errorf("failed to check access for data object %q: %w", trimmed, err) + } + + result.Results = append(result.Results, &ObjectAccessResult{ + DataObject: mapToDataAccessObject(ctx, obj), + Access: mapToUserDataObjectAccess(item), + }) + } + + return result, nil +} + +// resolveDataAccessUser resolves the user to check access for: by ID, then by email, then the +// current user. +func resolveDataAccessUser(ctx context.Context, collibraClient *sdk.CollibraClient, userID, email string) (*types.User, error) { + switch { + case strings.TrimSpace(userID) != "": + return collibraClient.User().GetUser(ctx, userID) + case strings.TrimSpace(email) != "": + return collibraClient.User().GetUserByEmail(ctx, email) + default: + return collibraClient.User().GetCurrentUser(ctx) + } +} + +func mapToUserDataObjectAccess(item *types.GroupedDataAccessReturnItem) *UserDataObjectAccess { + if item == nil { + return &UserDataObjectAccess{HasAccess: false, Roles: []DataObjectAccessRole{}} + } + + access := &UserDataObjectAccess{ + HasAccess: true, + Permissions: derefStrings(item.Permissions), + GlobalPermissions: derefStrings(item.GlobalPermissions), + ExpiresAt: item.ExpiresAt, + Roles: make([]DataObjectAccessRole, 0, len(item.NearestAccessControls)), + } + + for _, ac := range item.NearestAccessControls { + if ac == nil { + continue + } + role := DataObjectAccessRole{ + ID: ac.Id, + Name: ac.Name, + Action: string(ac.Action), + State: string(ac.State), + } + if ac.Category != nil { + role.Category = &DataAccessGrantCategory{ + ID: ac.Category.GrantCategory.Id, + Name: ac.Category.GrantCategory.Name, + NamePlural: ac.Category.GrantCategory.NamePlural, + IsSystem: ac.Category.GrantCategory.IsSystem, + IsDefault: ac.Category.GrantCategory.IsDefault, + } + } + access.Roles = append(access.Roles, role) + } + + return access +} + +// derefStrings dereferences a slice of string pointers, skipping nils. +func derefStrings(in []*string) []string { + if len(in) == 0 { + return nil + } + out := make([]string, 0, len(in)) + for _, s := range in { + if s != nil { + out = append(out, *s) + } + } + return out +} + // CreateDataAccessRequestWhatInput describes a single WHAT item (a data object) for a new // data access request, with optional requested permissions. type CreateDataAccessRequestWhatInput struct { @@ -384,7 +605,7 @@ func CreateDataAccessRequest(ctx context.Context, httpClient *http.Client, input return nil, err } - requestURL := strings.TrimSuffix(collibraHost, "/") + "/data-access/access-requests/" + ar.Id + uiURL := buildUiUrl(ctx, "access-requests", ar.Id) return &DataAccessRequestSummary{ ID: ar.Id, @@ -392,11 +613,12 @@ func CreateDataAccessRequest(ctx context.Context, httpClient *http.Client, input Description: ar.Description, Status: string(ar.Status), Outcome: string(ar.Outcome), - Url: requestURL, + Url: uiURL, }, nil } -func mapToDataAccessObject(o *types.DataObject) *DataAccessObject { +func mapToDataAccessObject(ctx context.Context, o *types.DataObject) *DataAccessObject { + uiURL := buildUiUrl(ctx, "data-objects", o.Id) out := &DataAccessObject{ ID: o.Id, Name: o.Name, @@ -405,6 +627,7 @@ func mapToDataAccessObject(o *types.DataObject) *DataAccessObject { DataType: o.DataType, Deleted: o.Deleted, Description: o.Description, + Url: uiURL, } if o.DataSource != nil { out.DataSourceID = o.DataSource.Id @@ -421,16 +644,23 @@ func mapToDataAccessObject(o *types.DataObject) *DataAccessObject { return out } -func mapToDataAccessIdentity(u *types.User) *DataAccessIdentity { +func mapToDataAccessIdentity(ctx context.Context, u *types.User) *DataAccessIdentity { + uiURL := buildUiUrl(ctx, "identities", u.Id) return &DataAccessIdentity{ ID: u.Id, Name: u.Name, Email: u.Email, Type: string(u.Type), + Url: uiURL, } } -func mapToDataAccessControlDetails(ac *types.AccessControl) *DataAccessControlDetails { +func mapToDataAccessControlDetails(ctx context.Context, ac *types.AccessControl) *DataAccessControlDetails { + categoryName := "default" + if ac.GetCategory() != nil { + categoryName = ac.GetCategory().Name + } + uiURL := buildUiUrl(ctx, "access-controls/"+categoryName, ac.Id) details := &DataAccessControlDetails{ What: []DataAccessWhatItem{}, Who: []DataAccessWhoItem{}, @@ -449,6 +679,7 @@ func mapToDataAccessControlDetails(ac *types.AccessControl) *DataAccessControlDe WhoUnknown: ac.WhoUnknown, CreatedAt: ac.CreatedAt, ModifiedAt: ac.ModifiedAt, + Url: uiURL, } if ac.Category != nil { @@ -472,3 +703,11 @@ func mapToDataAccessControlDetails(ac *types.AccessControl) *DataAccessControlDe return details } + +func buildUiUrl(ctx context.Context, resourceType string, id string) string { + collibraHost, ok := chip.GetCollibraHost(ctx) + if !ok { + return "" + } + return strings.TrimSuffix(collibraHost, "/") + "/data-access/" + resourceType + "/" + id +} diff --git a/pkg/skills/files/collibra/data-access/SKILL.md b/pkg/skills/files/collibra/data-access/SKILL.md new file mode 100644 index 0000000..94c4adb --- /dev/null +++ b/pkg/skills/files/collibra/data-access/SKILL.md @@ -0,0 +1,48 @@ +--- +description: Manages who can access what data, through grants, masks and filters. +related: collibra/discovery, collibra/index +--- + +# Data Access + +Data Access is the system that manages who can access what data, through grants, masks and filters. + +## Hard rules + +1. Data Access users are not necessarily DGC users. Data Access users are only also DGC users if their email address is the same. +2. Ownership of a data object does not mean that user has access to it. + +## Search identities + +`search_data_access_identities` is a tool to search for Data Access users (identities) by name and/or email. Providing `email` performs an exact lookup via `GetUserByEmail`. Providing `name` performs a server-side case-insensitive contains search via `SearchUsers`. Both can be combined: email resolves the user, name filters the result client-side. Name-only searches are paginated (25 per page) — use the returned `nextCursor` to fetch subsequent pages. + +## Search Data Access objects + +`search_data_access_objects` is a tool to search for data objects in Collibra Data Access (tables, columns, schemas, views, and other entities tracked in registered data sources). Filters can be combined: `name` (case-insensitive contains), `dataSources` (data source IDs), `types` (e.g. `table`, `column`, `schema`, `view`), `parents` / `ancestors` (other data object IDs to scope the search to a sub-tree), and `includeDeleted`. Returns up to `pageSize` matches (default 25, max 25). Each result includes the data object ID, name, fully qualified name, type, data type, deleted flag, description, data source ID, and `applicablePermissions` — the list of source-system permissions (each with a `name` and `description`) that can be requested on the object. Use those names when populating `what[].permissions` for `create_data_access_request`. + +## Resolve a data source + +`get_data_access_data_source` fetches a single data source by its **ID**, returning its `name`, `type` (e.g. `snowflake`, `databricks`, `bigquery`), `description`, `parentId`, and `createdAt` / `modifiedAt` timestamps. Use it to turn an opaque data source ID into a human-readable name. Data objects carry their data source as the `dataSourceId` field on results from `search_data_access_objects` and `check_user_data_object_access` — when the same object name exists in several data sources, fetch each `dataSourceId` to report which data source is which instead of printing raw IDs. If the ID does not resolve, the tool returns a `message` (and no `dataSource`); ask the user to correct it and call again. + +## Check user access to data objects + +`check_user_data_object_access` answers "Does a user have access to a data object (database, schema, table, view, column, etc.) and through which roles?". It takes one or more data object **IDs** in `objectIds` — not names. For every object it reports whether the user has access (`hasAccess`), the granted `permissions` and `globalPermissions`, and `roles` — the access controls that grant the access (each with `id`, `name`, `action`, `state`, and grant `category`). Required behavior: + +- **WHO defaults to the current user.** Leave `userId` and `email` empty to check the calling user. To check a different user, resolve them first via `search_data_access_identities` and pass the returned ID in `userId` (or pass the user's `email`). Never pass a raw name. +- **WHAT must be resolved to IDs first.** Use `search_data_access_objects` to find the data object the user means, then pass its `id` in `objectIds`. When the name the user gave matches several objects (e.g. the same table in different schemas or data sources), present the candidates and let the user pick before checking access — do not guess. +- **Handle unresolved IDs.** IDs that do not correspond to an existing data object (`reason: not_found`) are returned in `result.unresolved`, and the tool sets a `message`. Ask the user to correct or drop those IDs, then call again. Objects that did resolve are still reported in `result.results`. +- **`expiresAt` caveat.** It is only populated when access is granted through a single access control; when multiple roles grant access it is `null`. + +## Create Data Access request + +`create_data_access_request` is a tool to create a new Collibra Data Access request on behalf of one or more users for one or more data objects. Destructive. Required behavior: + +- **Minimum input is WHO, WHAT, and a purpose.** Do not call this tool until all three are supplied. +- **WHO** must be resolved via `search_data_access_identities` (by email or name) — pass the returned user IDs in `userIds`. Never pass raw emails or names. +- **WHAT** must be resolved via `search_data_access_objects` — pass the returned data object IDs in `what[].dataObjectId`. Per item, `permissions` should be empty and `globalPermissions` must always be READ. +- **Purpose** is mandatory and must come from the user — it is the business justification for the request. If the user has not stated a purpose, ask them for one before calling the tool. Do not invent a purpose. The tool always appends a note stating that the request was created by AI. +- **Name** is optional. If the user does not provide one, omit `name` on the first call. The tool will return status `needs_name_confirmation` with a `suggestedName` derived from the purpose — present that suggestion to the user, get their confirmation (or an alternative), and call again with the confirmed value in `name`. + +## Common follow-ups + +- Found a **data source id** → `get_data_access_data_source` to resolve its `name` and `type` (e.g. `snowflake`, `databricks`, `bigquery`). diff --git a/pkg/tools/check_user_data_object_access/tool.go b/pkg/tools/check_user_data_object_access/tool.go new file mode 100644 index 0000000..2444d9b --- /dev/null +++ b/pkg/tools/check_user_data_object_access/tool.go @@ -0,0 +1,63 @@ +package check_user_data_object_access + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/collibra/chip/pkg/chip" + "github.com/collibra/chip/pkg/clients" + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +type Input struct { + ObjectIds []string `json:"objectIds" jsonschema:"Required. One or more data object IDs (database, schema, table, view, column, etc.) to check access for. Obtain IDs via search_data_access_objects."` + UserID string `json:"userId,omitempty" jsonschema:"Optional. ID of the user to check access for. Resolve names/emails to a user ID via search_data_access_identities. When omitted along with email, the current user is used."` + Email string `json:"email,omitempty" jsonschema:"Optional. Email of the user to check access for, used when userId is not supplied. When omitted along with userId, the current user is used."` +} + +type Output struct { + Result *clients.CheckUserDataObjectAccessResult `json:"result,omitempty" jsonschema:"The access check result: the resolved user, per-object access (with granting roles), and any IDs that could not be resolved."` + Message string `json:"message,omitempty" jsonschema:"Guidance for the agent, set when one or more IDs could not be resolved — ask the user to correct or drop them."` + Error string `json:"error,omitempty" jsonschema:"Error message if the access check could not be completed."` +} + +func NewTool(collibraClient *http.Client) *chip.Tool[Input, Output] { + return &chip.Tool[Input, Output]{ + Name: "check_user_data_object_access", + Description: "Checks if a user has access to a data object (database, schema, table, view, column, etc.) and through which access controls (ie roles). Takes one or more data object IDs (obtain them via search_data_access_objects). For every object it reports whether the user has access, the granted permissions, and the access controls (roles) that grant the access. Checks the current user unless userId or email is supplied (resolve names via search_data_access_identities). IDs that do not correspond to an existing data object are returned in result.unresolved with a message asking the user to correct or drop them.", + Handler: handle(collibraClient), + Permissions: []string{}, + Annotations: &mcp.ToolAnnotations{ReadOnlyHint: true, DestructiveHint: new(false)}, + } +} + +func handle(collibraClient *http.Client) chip.ToolHandlerFunc[Input, Output] { + return func(ctx context.Context, input Input) (Output, error) { + ids := make([]string, 0, len(input.ObjectIds)) + for _, id := range input.ObjectIds { + if strings.TrimSpace(id) != "" { + ids = append(ids, id) + } + } + if len(ids) == 0 { + return Output{Error: "at least one object ID is required"}, nil + } + + result, err := clients.CheckUserDataObjectAccess(ctx, collibraClient, ids, input.UserID, input.Email) + if err != nil { + return Output{Error: fmt.Sprintf("Failed to check data object access: %s", err.Error())}, nil + } + + out := Output{Result: result} + if len(result.Unresolved) > 0 { + unresolved := make([]string, 0, len(result.Unresolved)) + for _, u := range result.Unresolved { + unresolved = append(unresolved, fmt.Sprintf("%q (%s)", u.ID, u.Reason)) + } + out.Message = fmt.Sprintf("Could not resolve %d ID(s) to a data object: %s. Ask the user to correct or drop them, then call again.", len(result.Unresolved), strings.Join(unresolved, ", ")) + } + return out, nil + } +} diff --git a/pkg/tools/check_user_data_object_access/tool_test.go b/pkg/tools/check_user_data_object_access/tool_test.go new file mode 100644 index 0000000..58af607 --- /dev/null +++ b/pkg/tools/check_user_data_object_access/tool_test.go @@ -0,0 +1,155 @@ +package check_user_data_object_access_test + +import ( + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/collibra/chip/pkg/chip" + tool "github.com/collibra/chip/pkg/tools/check_user_data_object_access" + "github.com/collibra/chip/pkg/tools/testutil" +) + +// The data access SDK posts all queries to the same GraphQL endpoint (/dataAccess/query). +// Tests dispatch on the operation name embedded in the query body. +const gqlPath = "/dataAccess/query" + +const currentUserResp = `{"data":{"currentUser":{"id":"user-1","name":"Alice","email":"alice@example.com","type":"Human"}}}` + +const accessResp = `{"data":{"dataObject":{"distinctAccess":{"__typename":"GroupedDataAccessReturnItemConnection",` + + `"pageInfo":{"hasNextPage":false,"startCursor":null},` + + `"edges":[{"cursor":"a1","node":{"permissions":["SELECT"],"globalPermissions":["READ"],"expiresAt":null,` + + `"user":{"id":"user-1","name":"Alice","email":"alice@example.com","type":"Human"},` + + `"nearestAccessControls":[{"id":"ac-1","name":"Analysts","action":"Grant","state":"Active",` + + `"category":{"id":"cat-1","name":"Read","namePlural":"Reads","isSystem":true,"isDefault":true}}]}}]}}}}` + +func dataObjectResp(id, name, fullName string) string { + return `{"data":{"dataObject":{"id":"` + id + `","name":"` + name + `","fullName":"` + fullName + + `","type":"table","dataType":null,"deleted":false,"description":"","dataSource":{"id":"ds-1"},"applicablePermissions":[]}}}` +} + +// emptyDataObjectResp mimics the GraphQL response for an ID that matches no data object: the +// dataObject field is null, which genqlient decodes to a zero-value object (empty id). +const emptyDataObjectResp = `{"data":{"dataObject":null}}` + +// newGQLServer returns a test server that answers the three operations the tool relies on. The +// GetDataObject response is supplied by the caller so each test can control ID resolution. +func newGQLServer(t *testing.T, getObjectResp string) *httptest.Server { + t.Helper() + handler := http.NewServeMux() + handler.HandleFunc(gqlPath, func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + q := string(body) + w.Header().Set("Content-Type", "application/json") + switch { + case strings.Contains(q, "query CurrentUser"): + _, _ = io.WriteString(w, currentUserResp) + case strings.Contains(q, "query GetDataObjectAccessList"): + _, _ = io.WriteString(w, accessResp) + case strings.Contains(q, "query GetDataObject"): + _, _ = io.WriteString(w, getObjectResp) + default: + http.Error(w, "unexpected query: "+q, http.StatusBadRequest) + } + }) + return httptest.NewServer(handler) +} + +func TestCheckUserDataObjectAccess_HasAccess(t *testing.T) { + server := newGQLServer(t, dataObjectResp("do-1", "customers", "db.public.customers")) + defer server.Close() + + ctx := chip.SetCollibraHost(t.Context(), "http://collibra.test") + output, err := tool.NewTool(testutil.NewClient(server)).Handler(ctx, tool.Input{ + ObjectIds: []string{"do-1"}, + }) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + if output.Error != "" { + t.Fatalf("Expected no tool error, got: %q", output.Error) + } + if output.Message != "" { + t.Fatalf("Expected no message, got: %q", output.Message) + } + if output.Result == nil { + t.Fatal("Expected a result") + } + if output.Result.User == nil || output.Result.User.ID != "user-1" { + t.Fatalf("Expected user-1, got: %+v", output.Result.User) + } + if len(output.Result.Unresolved) != 0 { + t.Fatalf("Expected no unresolved names, got: %+v", output.Result.Unresolved) + } + if len(output.Result.Results) != 1 { + t.Fatalf("Expected 1 result, got: %d", len(output.Result.Results)) + } + + res := output.Result.Results[0] + if res.DataObject == nil || res.DataObject.ID != "do-1" { + t.Fatalf("Unexpected resolved object: %+v", res.DataObject) + } + if res.Access == nil || !res.Access.HasAccess { + t.Fatalf("Expected hasAccess true, got: %+v", res.Access) + } + if len(res.Access.Permissions) != 1 || res.Access.Permissions[0] != "SELECT" { + t.Fatalf("Expected permissions [SELECT], got: %v", res.Access.Permissions) + } + if len(res.Access.GlobalPermissions) != 1 || res.Access.GlobalPermissions[0] != "READ" { + t.Fatalf("Expected globalPermissions [READ], got: %v", res.Access.GlobalPermissions) + } + if len(res.Access.Roles) != 1 { + t.Fatalf("Expected 1 role, got: %d", len(res.Access.Roles)) + } + role := res.Access.Roles[0] + if role.ID != "ac-1" || role.Name != "Analysts" || role.Action != "Grant" || role.State != "Active" { + t.Fatalf("Unexpected role: %+v", role) + } + if role.Category == nil || role.Category.Name != "Read" { + t.Fatalf("Expected role category Read, got: %+v", role.Category) + } +} + +func TestCheckUserDataObjectAccess_UnresolvedNotFound(t *testing.T) { + server := newGQLServer(t, emptyDataObjectResp) + defer server.Close() + + ctx := chip.SetCollibraHost(t.Context(), "http://collibra.test") + output, err := tool.NewTool(testutil.NewClient(server)).Handler(ctx, tool.Input{ + ObjectIds: []string{"ghost"}, + }) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + if output.Result == nil { + t.Fatal("Expected a result") + } + if len(output.Result.Results) != 0 { + t.Fatalf("Expected no resolved results, got: %d", len(output.Result.Results)) + } + if len(output.Result.Unresolved) != 1 { + t.Fatalf("Expected 1 unresolved ID, got: %+v", output.Result.Unresolved) + } + if output.Result.Unresolved[0].ID != "ghost" || output.Result.Unresolved[0].Reason != "not_found" { + t.Fatalf("Unexpected unresolved entry: %+v", output.Result.Unresolved[0]) + } + if output.Message == "" { + t.Fatal("Expected a message asking the user to correct or drop the ID") + } +} + +func TestCheckUserDataObjectAccess_RequiresIDs(t *testing.T) { + output, err := tool.NewTool(nil).Handler(t.Context(), tool.Input{ObjectIds: []string{" "}}) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + if output.Error == "" { + t.Fatal("Expected an error when no object IDs are supplied") + } +} diff --git a/pkg/tools/register.go b/pkg/tools/register.go index f3f5e86..235580a 100644 --- a/pkg/tools/register.go +++ b/pkg/tools/register.go @@ -5,14 +5,18 @@ import ( "net/http" "github.com/collibra/chip/pkg/chip" + "github.com/collibra/chip/pkg/skills" "github.com/collibra/chip/pkg/tools/add_data_classification_match" + "github.com/collibra/chip/pkg/tools/check_user_data_object_access" "github.com/collibra/chip/pkg/tools/create_asset" + "github.com/collibra/chip/pkg/tools/create_data_access_request" "github.com/collibra/chip/pkg/tools/discover_business_glossary" "github.com/collibra/chip/pkg/tools/discover_data_assets" "github.com/collibra/chip/pkg/tools/edit_asset" "github.com/collibra/chip/pkg/tools/get_asset_details" "github.com/collibra/chip/pkg/tools/get_business_term_data" "github.com/collibra/chip/pkg/tools/get_column_semantics" + "github.com/collibra/chip/pkg/tools/get_data_access_data_source" "github.com/collibra/chip/pkg/tools/get_debug_mcp_init_request" "github.com/collibra/chip/pkg/tools/get_lineage_downstream" "github.com/collibra/chip/pkg/tools/get_lineage_entity" @@ -27,11 +31,12 @@ import ( "github.com/collibra/chip/pkg/tools/push_data_contract_manifest" "github.com/collibra/chip/pkg/tools/remove_data_classification_match" "github.com/collibra/chip/pkg/tools/search_asset_keyword" + "github.com/collibra/chip/pkg/tools/search_data_access_identities" + "github.com/collibra/chip/pkg/tools/search_data_access_objects" "github.com/collibra/chip/pkg/tools/search_data_classes" "github.com/collibra/chip/pkg/tools/search_data_classification_matches" "github.com/collibra/chip/pkg/tools/search_lineage_entities" "github.com/collibra/chip/pkg/tools/search_lineage_transformations" - "github.com/collibra/chip/pkg/skills" ) // CopilotToolNames lists tool names that are routed to the copilot service. @@ -68,6 +73,11 @@ func RegisterAll(server *chip.Server, client *http.Client, toolConfig *chip.Serv toolRegister(server, toolConfig, prepare_create_asset.NewTool(client)) toolRegister(server, toolConfig, create_asset.NewTool(client)) toolRegister(server, toolConfig, edit_asset.NewTool(client)) + toolRegister(server, toolConfig, search_data_access_identities.NewTool(client)) + toolRegister(server, toolConfig, search_data_access_objects.NewTool(client)) + toolRegister(server, toolConfig, create_data_access_request.NewTool(client)) + toolRegister(server, toolConfig, check_user_data_object_access.NewTool(client)) + toolRegister(server, toolConfig, get_data_access_data_source.NewTool(client)) if toolConfig.EnableDebugTools { toolRegister(server, toolConfig, get_debug_mcp_init_request.NewTool(client)) From ad99e9881d03d56e88ceff0d1118c6d9ef803c5d Mon Sep 17 00:00:00 2001 From: Wouter Cordewiner <73988312+wouterc-collibra@users.noreply.github.com> Date: Tue, 28 Jul 2026 08:59:57 +0200 Subject: [PATCH 08/13] Add data access skills --- go.mod | 7 ++++++- go.sum | 35 +++++++++++++++++++++++++---------- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/go.mod b/go.mod index ada5656..1790f97 100644 --- a/go.mod +++ b/go.mod @@ -14,8 +14,12 @@ require ( ) require ( + github.com/Khan/genqlient v0.8.2-0.20260527022710-6bbde3684dd6 // indirect + github.com/Masterminds/semver/v3 v3.5.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-retryablehttp v0.7.8 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/sagikazarmark/locafero v0.12.0 // indirect @@ -24,10 +28,11 @@ require ( github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/vektah/gqlparser/v2 v2.5.36 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/oauth2 v0.35.0 // indirect golang.org/x/sys v0.41.0 // indirect golang.org/x/text v0.32.0 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index e9b55cf..41bc789 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,13 @@ -github.com/collibra/data-access-go-sdk v0.0.69/go.mod h1:oT1RYAOH5ZVZzjo4ut96QjtZLshqBXIieeVOstVyRTw= +github.com/Khan/genqlient v0.8.2-0.20260527022710-6bbde3684dd6 h1:Y+TbviEJfdsIS2jwgSLkSK2t0O1eVaRsvtW5oPQ9it4= +github.com/Khan/genqlient v0.8.2-0.20260527022710-6bbde3684dd6/go.mod h1:guUHwMi8ByjIvs3TyAPM+V9ryaW305CtK7+aCeP2Jzc= +github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= +github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/collibra/data-access-go-sdk v0.0.70 h1:1GMfb1vhNc2IXpUqTyibutfBqqjSKDWUzldiZywR2Y8= github.com/collibra/data-access-go-sdk v0.0.70/go.mod h1:oT1RYAOH5ZVZzjo4ut96QjtZLshqBXIieeVOstVyRTw= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= @@ -19,19 +25,26 @@ github.com/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+ github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= +github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/modelcontextprotocol/go-sdk v1.6.1 h1:0zOSupjKUxPKSocPT1Wtago+mUHU2/uZ4xSOY0FGReU= github.com/modelcontextprotocol/go-sdk v1.6.1/go.mod h1:kzm3kzFL1/+AziGOE0nUs3gvPoNxMCvkxokMkuFapXQ= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= @@ -52,6 +65,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/vektah/gqlparser/v2 v2.5.36 h1:CN9mKVHgMkc+XftdOWIhb4HEL8wKSYkFAqhf8booa7s= +github.com/vektah/gqlparser/v2 v2.5.36/go.mod h1:cAJ9qwVgPaUkWv6Gn8vn0mqOE0Ui5Pn56wNy5396XWo= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= github.com/yuin/goldmark v1.8.4 h1:oat/nd3U6NeQqFEL3xpEJq7d7c86NI+DbSNGAs4xnjA= @@ -64,8 +79,8 @@ golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= -golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= -golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= +golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= From cd8d2c07488b9c7106b3d904d741209709c0def7 Mon Sep 17 00:00:00 2001 From: Wouter Cordewiner <73988312+wouterc-collibra@users.noreply.github.com> Date: Wed, 5 Aug 2026 22:10:09 +0200 Subject: [PATCH 09/13] Merge with main --- go.mod | 10 ++++++++-- go.sum | 38 +++++++++++++++++++++++++++++--------- 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/go.mod b/go.mod index b661198..3d3c3ab 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,9 @@ module github.com/collibra/chip -go 1.25.0 +go 1.26.5 require ( + github.com/collibra/data-access-go-sdk v0.0.71 github.com/google/go-querystring v1.2.0 github.com/google/jsonschema-go v0.4.3 github.com/google/uuid v1.6.0 @@ -13,8 +14,12 @@ require ( ) require ( + github.com/Khan/genqlient v0.8.2-0.20260527022710-6bbde3684dd6 // indirect + github.com/Masterminds/semver/v3 v3.5.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-retryablehttp v0.7.8 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/sagikazarmark/locafero v0.12.0 // indirect @@ -23,10 +28,11 @@ require ( github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/vektah/gqlparser/v2 v2.5.36 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/oauth2 v0.35.0 // indirect golang.org/x/sys v0.45.0 // indirect golang.org/x/text v0.34.0 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 41bffe3..f358e74 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,15 @@ -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/Khan/genqlient v0.8.2-0.20260527022710-6bbde3684dd6 h1:Y+TbviEJfdsIS2jwgSLkSK2t0O1eVaRsvtW5oPQ9it4= +github.com/Khan/genqlient v0.8.2-0.20260527022710-6bbde3684dd6/go.mod h1:guUHwMi8ByjIvs3TyAPM+V9ryaW305CtK7+aCeP2Jzc= +github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= +github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/collibra/data-access-go-sdk v0.0.70 h1:1GMfb1vhNc2IXpUqTyibutfBqqjSKDWUzldiZywR2Y8= +github.com/collibra/data-access-go-sdk v0.0.70/go.mod h1:oT1RYAOH5ZVZzjo4ut96QjtZLshqBXIieeVOstVyRTw= +github.com/collibra/data-access-go-sdk v0.0.71 h1:r6sQJbQa0ICRBMwBVxX/aVJPqKvNq9s4SwM7EU/0Ic8= +github.com/collibra/data-access-go-sdk v0.0.71/go.mod h1:oT1RYAOH5ZVZzjo4ut96QjtZLshqBXIieeVOstVyRTw= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= @@ -17,20 +27,28 @@ github.com/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+ github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= +github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/modelcontextprotocol/go-sdk v1.6.1 h1:0zOSupjKUxPKSocPT1Wtago+mUHU2/uZ4xSOY0FGReU= github.com/modelcontextprotocol/go-sdk v1.6.1/go.mod h1:kzm3kzFL1/+AziGOE0nUs3gvPoNxMCvkxokMkuFapXQ= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc= @@ -49,6 +67,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/vektah/gqlparser/v2 v2.5.36 h1:CN9mKVHgMkc+XftdOWIhb4HEL8wKSYkFAqhf8booa7s= +github.com/vektah/gqlparser/v2 v2.5.36/go.mod h1:cAJ9qwVgPaUkWv6Gn8vn0mqOE0Ui5Pn56wNy5396XWo= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= github.com/yuin/goldmark v1.8.4 h1:oat/nd3U6NeQqFEL3xpEJq7d7c86NI+DbSNGAs4xnjA= @@ -61,8 +81,8 @@ golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= -golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= -golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= +golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= From af7f20129cdb9a6f2fb26a8c56847239b4f50e1b Mon Sep 17 00:00:00 2001 From: Wouter Cordewiner <73988312+wouterc-collibra@users.noreply.github.com> Date: Wed, 5 Aug 2026 22:46:05 +0200 Subject: [PATCH 10/13] fix(data-access): register orphaned tool, fix broken README links and skill doc get_data_access_control_details was fully implemented but never wired into RegisterAll after a branch merge dropped it. Also fixes README links that pointed to the wrong tool directories and removes a stale nextCursor pagination claim in the data-access SKILL.md that doesn't match the search_data_access_identities implementation. Co-Authored-By: Claude Sonnet 5 --- README.md | 7 ++++--- pkg/clients/data_access_client.go | 6 ------ pkg/skills/files/collibra/data-access/SKILL.md | 6 +++++- pkg/tools/register.go | 2 ++ 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 8e0a13d..9165679 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ This Go-based MCP server acts as a bridge between AI applications and Collibra, - [`get_asset_details`](pkg/tools/get_asset_details/) - Retrieve detailed information about specific assets by UUID, including the asset's assignable attribute schema (every attribute it can hold, including empty ones) - [`get_business_term_data`](pkg/tools/get_business_term_data/) - Trace a business term back to its connected physical data assets - [`get_column_semantics`](pkg/tools/get_column_semantics/) - Retrieve data attributes, measures, and business assets connected to a column +- [`get_data_access_control_details`](pkg/tools/get_data_access_control_details/) - Retrieve detailed information about a specific Collibra Data Access control by its id - [`get_data_access_data_source`](pkg/tools/get_data_access_data_source/) - Fetch a Collibra Data Access data source by ID, resolving an opaque data source ID to its name, type, and description - [`get_data_quality_rule`](pkg/tools/get_dq_rule/) - Read the definition of a single DQ rule (monitor) on a job — its type, SQL, filter, tolerance and active/suppressed state - [`get_data_quality_rule_results`](pkg/tools/get_dq_rule_results/) - Read a rule's per-run results after a job run — score, breaking/passing record counts, pass/fail status and any exception. Paginated (`offset`/`limit`), newest first by default @@ -37,8 +38,8 @@ This Go-based MCP server acts as a bridge between AI applications and Collibra, - [`pull_data_contract_manifest`](pkg/tools/pull_data_contract_manifest/) - Download manifest for a data contract - [`search_asset_keyword`](pkg/tools/search_asset_keyword/) - Wildcard keyword search for assets; filters (status, community, domain, domain type, asset type, created-by) accept names or UUIDs - [`search_catalog_columns`](pkg/tools/search_catalog_columns/) - Find catalog Column assets by metadata that keyword search can't filter on — Description/Data Type (attribute values), a Data Steward role, or relations to a Business Term/Business Rule/Data Element/Data Attribute (by name); AND-combined. Uses the DGC Knowledge Graph GraphQL API (must be enabled on the instance). Classification-tag filtering is not supported -- [`search_data_access_identities`](pkg/tools/search_lineage_transformations/) - Search for Data Access users (identities) by name and/or email -- [`search_data_access_objects`](pkg/tools/search_lineage_transformations/) - Search for data objects in Collibra Data Access (tables, columns, schemas, views, and other entities tracked in registered data sources) +- [`search_data_access_identities`](pkg/tools/search_data_access_identities/) - Search for Data Access users (identities) by name and/or email +- [`search_data_access_objects`](pkg/tools/search_data_access_objects/) - Search for data objects in Collibra Data Access (tables, columns, schemas, views, and other entities tracked in registered data sources) - [`search_data_class`](pkg/tools/search_data_classes/) - Search for data classes with filters. **Requires:** `dgc.data-classes-read` - [`search_data_classification_match`](pkg/tools/search_data_classification_matches/) - Search for associations between data classes and assets. **Requires:** `dgc.classify`, `dgc.catalog` - [`search_lineage_entities`](pkg/tools/search_lineage_entities/) - Search for entities in the technical lineage graph @@ -49,7 +50,7 @@ This Go-based MCP server acts as a bridge between AI applications and Collibra, - [`add_data_classification_match`](pkg/tools/add_data_classification_match/) - Associate a data class with an asset. **Requires:** `dgc.classify`, `dgc.catalog` - [`create_assessment`](pkg/tools/create_assessment/) - Conduct a new assessment from a template (given by name or UUID) in the Assessments application. Returns the template's (unanswered) questions to fill in afterward with `edit_assessment` — no separate prepare step needed - [`create_asset`](pkg/tools/create_asset/) - Create a new asset of any type. Resolves `assetType` (UUID, publicId, or display name), `domain` (UUID or name), `status` (UUID or name), and attributes (by name or typeId) server-side; converts Markdown to HTML for `RICH_TEXT` attributes; gates on duplicate-name (default `allowDuplicate: false`) -- [`create_data_access_request`](pkg/tools/create_asset/) - Create a new Collibra Data Access request on behalf of one or more users for one or more data objects +- [`create_data_access_request`](pkg/tools/create_data_access_request/) - Create a new Collibra Data Access request on behalf of one or more users for one or more data objects - [`create_data_quality_rule`](pkg/tools/create_dq_rule/) - Create a data quality rule (monitor) on an existing DQ job. `monitorType` is `FREEFORM_SQL` (full SQL query) or `SIMPLE_SQL` (single-column check); defaults to active and not suppressed. Confirm checkpoint: `confirm=false` (default) returns a preview of the rule + SQL without creating; `confirm=true` creates. Uses the DQ monitoring API and requires permission to create rules on the target job. **Experimental** (`data-quality` feature flag) - [`deploy_data_quality_rule_template`](pkg/tools/deploy_dq_rule_template/) - Instantiate a rule template as concrete rules across one or more job/column targets (bulk). The DQ service resolves dialect-specific SQL and names each rule `{templateName}_{columnName}`. Confirm checkpoint: `confirm=false` (default) previews the template + targets without deploying; `confirm=true` deploys. Requires permission to deploy templates and create rules on the target jobs. **Experimental** (`data-quality` feature flag) - [`edit_assessment`](pkg/tools/edit_assessment/) - Edit a conducted assessment (identified by name or UUID) via a list of typed operations, applied as a single atomic PATCH (all-or-nothing): diff --git a/pkg/clients/data_access_client.go b/pkg/clients/data_access_client.go index b515fa4..e362a1c 100644 --- a/pkg/clients/data_access_client.go +++ b/pkg/clients/data_access_client.go @@ -118,12 +118,6 @@ func GetDataAccessControl(ctx context.Context, httpClient *http.Client, id strin return details, nil } -// SearchDataAccessControlsResult holds a page of access controls and an optional next-page cursor. -type SearchDataAccessControlsResult struct { - Items []*DataAccessControlDetails `json:"items"` - NextCursor *string `json:"nextCursor,omitempty"` -} - func mapToDataAccessWhatItem(w *types.AccessWhatAccessControlItem) DataAccessWhatItem { item := DataAccessWhatItem{ ExpiresAt: w.ExpiresAt, diff --git a/pkg/skills/files/collibra/data-access/SKILL.md b/pkg/skills/files/collibra/data-access/SKILL.md index 94c4adb..0e8c405 100644 --- a/pkg/skills/files/collibra/data-access/SKILL.md +++ b/pkg/skills/files/collibra/data-access/SKILL.md @@ -14,7 +14,7 @@ Data Access is the system that manages who can access what data, through grants, ## Search identities -`search_data_access_identities` is a tool to search for Data Access users (identities) by name and/or email. Providing `email` performs an exact lookup via `GetUserByEmail`. Providing `name` performs a server-side case-insensitive contains search via `SearchUsers`. Both can be combined: email resolves the user, name filters the result client-side. Name-only searches are paginated (25 per page) — use the returned `nextCursor` to fetch subsequent pages. +`search_data_access_identities` is a tool to search for Data Access users (identities) by name and/or email. Providing `email` performs an exact lookup via `GetUserByEmail`. Providing `name` performs a server-side case-insensitive contains search via `SearchUsers`. Both can be combined: email resolves the user, name filters the result client-side. Name-only searches return up to `pageSize` matches (default 25, max 25); narrow the `name` filter to find results beyond that cap. ## Search Data Access objects @@ -43,6 +43,10 @@ Data Access is the system that manages who can access what data, through grants, - **Purpose** is mandatory and must come from the user — it is the business justification for the request. If the user has not stated a purpose, ask them for one before calling the tool. Do not invent a purpose. The tool always appends a note stating that the request was created by AI. - **Name** is optional. If the user does not provide one, omit `name` on the first call. The tool will return status `needs_name_confirmation` with a `suggestedName` derived from the purpose — present that suggestion to the user, get their confirmation (or an alternative), and call again with the confirmed value in `name`. +## Inspect an access control + +`get_data_access_control_details` fetches a single Collibra Data Access control by its **ID**, returning its name, description, state (`ACTIVE`/`INACTIVE`/`DELETED`), action type (`GRANT`/`MASK`/`FILTER`/`SHARE`/`GROUP`/`FILTERRULE`), grant category, policy rule, external-management status, and the full `what`/`who` scope lists. Use this to inspect an individual access control once you have its ID — for example, from a `roles` entry returned by `check_user_data_object_access`. + ## Common follow-ups - Found a **data source id** → `get_data_access_data_source` to resolve its `name` and `type` (e.g. `snowflake`, `databricks`, `bigquery`). diff --git a/pkg/tools/register.go b/pkg/tools/register.go index 39df0c8..2a355d8 100644 --- a/pkg/tools/register.go +++ b/pkg/tools/register.go @@ -25,6 +25,7 @@ import ( "github.com/collibra/chip/pkg/tools/get_business_term_data" "github.com/collibra/chip/pkg/tools/get_column_semantics" "github.com/collibra/chip/pkg/tools/get_context_specification" + "github.com/collibra/chip/pkg/tools/get_data_access_control_details" "github.com/collibra/chip/pkg/tools/get_data_access_data_source" "github.com/collibra/chip/pkg/tools/get_debug_mcp_init_request" "github.com/collibra/chip/pkg/tools/get_dq_rule" @@ -106,6 +107,7 @@ func RegisterAll(server *chip.Server, client *http.Client, toolConfig *chip.Serv toolRegister(server, toolConfig, create_data_access_request.NewTool(client)) toolRegister(server, toolConfig, check_user_data_object_access.NewTool(client)) toolRegister(server, toolConfig, get_data_access_data_source.NewTool(client)) + toolRegister(server, toolConfig, get_data_access_control_details.NewTool(client)) toolRegister(server, toolConfig, get_assessment.NewTool(client)) toolRegister(server, toolConfig, create_assessment.NewTool(client)) toolRegister(server, toolConfig, edit_assessment.NewTool(client)) From 0b4f8671b77bcad4e0738c16d821c2ffe12fee9e Mon Sep 17 00:00:00 2001 From: Wouter Cordewiner <73988312+wouterc-collibra@users.noreply.github.com> Date: Wed, 5 Aug 2026 22:54:36 +0200 Subject: [PATCH 11/13] ci: bump Go toolchain to 1.26.5 to match go.mod go.mod requires go 1.26.5 (data-access-go-sdk needs >=1.26.4), but CI was pinned to 1.25.0 with GOTOOLCHAIN=local, failing the build step. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/build.yaml | 2 +- .github/workflows/release.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 74f72ed..6ca271b 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -26,7 +26,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v6 with: - go-version: '1.25.0' + go-version: '1.26.5' - name: Install dependencies run: go mod download diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 43d825e..b2e9efe 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -50,7 +50,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v6 with: - go-version: '1.25.0' + go-version: '1.26.5' - name: Install dependencies run: go mod download From 56cd818dbd91c8c8f1d7332547f49cb4e7d127a9 Mon Sep 17 00:00:00 2001 From: Wouter Cordewiner <73988312+wouterc-collibra@users.noreply.github.com> Date: Wed, 5 Aug 2026 23:05:55 +0200 Subject: [PATCH 12/13] fix(data-access): add missing tool annotations Six data-access tools were missing Title and required mcp.ToolAnnotations fields (OpenWorldHint, IdempotentHint for read-only tools), failing TestRegisterAll_AllToolsHaveProperAnnotations. Co-Authored-By: Claude Sonnet 5 --- pkg/tools/check_user_data_object_access/tool.go | 3 ++- pkg/tools/create_data_access_request/tool.go | 3 ++- pkg/tools/get_data_access_control_details/tool.go | 3 ++- pkg/tools/get_data_access_data_source/tool.go | 3 ++- pkg/tools/search_data_access_identities/tool.go | 3 ++- pkg/tools/search_data_access_objects/tool.go | 3 ++- 6 files changed, 12 insertions(+), 6 deletions(-) diff --git a/pkg/tools/check_user_data_object_access/tool.go b/pkg/tools/check_user_data_object_access/tool.go index 2444d9b..cb04bfb 100644 --- a/pkg/tools/check_user_data_object_access/tool.go +++ b/pkg/tools/check_user_data_object_access/tool.go @@ -26,10 +26,11 @@ type Output struct { func NewTool(collibraClient *http.Client) *chip.Tool[Input, Output] { return &chip.Tool[Input, Output]{ Name: "check_user_data_object_access", + Title: "Check User Data Object Access", Description: "Checks if a user has access to a data object (database, schema, table, view, column, etc.) and through which access controls (ie roles). Takes one or more data object IDs (obtain them via search_data_access_objects). For every object it reports whether the user has access, the granted permissions, and the access controls (roles) that grant the access. Checks the current user unless userId or email is supplied (resolve names via search_data_access_identities). IDs that do not correspond to an existing data object are returned in result.unresolved with a message asking the user to correct or drop them.", Handler: handle(collibraClient), Permissions: []string{}, - Annotations: &mcp.ToolAnnotations{ReadOnlyHint: true, DestructiveHint: new(false)}, + Annotations: &mcp.ToolAnnotations{ReadOnlyHint: true, DestructiveHint: new(false), IdempotentHint: true, OpenWorldHint: new(false)}, } } diff --git a/pkg/tools/create_data_access_request/tool.go b/pkg/tools/create_data_access_request/tool.go index b96ba08..9b550ad 100644 --- a/pkg/tools/create_data_access_request/tool.go +++ b/pkg/tools/create_data_access_request/tool.go @@ -42,10 +42,11 @@ type Output struct { func NewTool(collibraClient *http.Client) *chip.Tool[Input, Output] { return &chip.Tool[Input, Output]{ Name: "create_data_access_request", + Title: "Create Data Access Request", Description: "Create a new Collibra Data Access request. Requires the WHO (beneficiary user IDs, obtained via search_data_access_identities), the WHAT (data objects, obtained via search_data_access_objects), and a user-supplied purpose that is used as the description. If no name is supplied, the tool returns a suggested name derived from the purpose with status needs_name_confirmation — confirm the suggestion (or get a replacement) with the user, then call again with name set. The description always ends with a note stating that the request was created by AI.", Handler: handle(collibraClient), Permissions: []string{}, - Annotations: &mcp.ToolAnnotations{ReadOnlyHint: false, DestructiveHint: new(false)}, + Annotations: &mcp.ToolAnnotations{ReadOnlyHint: false, DestructiveHint: new(false), OpenWorldHint: new(false)}, } } diff --git a/pkg/tools/get_data_access_control_details/tool.go b/pkg/tools/get_data_access_control_details/tool.go index 328d018..516d8ec 100644 --- a/pkg/tools/get_data_access_control_details/tool.go +++ b/pkg/tools/get_data_access_control_details/tool.go @@ -23,10 +23,11 @@ type DataAccessControlOutput struct { func NewTool(collibraClient *http.Client) *chip.Tool[DataAccessControlInput, DataAccessControlOutput] { return &chip.Tool[DataAccessControlInput, DataAccessControlOutput]{ Name: "get_data_access_control_details", + Title: "Get Data Access Control Details", Description: "Retrieve detailed information about a specific Collibra Data Access control by its id. Returns the access control's name, description, state (ACTIVE, INACTIVE, DELETED), action type (GRANT, MASK, FILTER, SHARE, GROUP, FILTERRULE), grant category, policy rule, external management status, ABAC scope parse status, and timestamps. Use this to inspect an individual access control when you know its ID.", Handler: handleGetDataAccessControlDetails(collibraClient), Permissions: []string{}, - Annotations: &mcp.ToolAnnotations{ReadOnlyHint: true, DestructiveHint: new(false)}, + Annotations: &mcp.ToolAnnotations{ReadOnlyHint: true, DestructiveHint: new(false), IdempotentHint: true, OpenWorldHint: new(false)}, } } diff --git a/pkg/tools/get_data_access_data_source/tool.go b/pkg/tools/get_data_access_data_source/tool.go index cb709a4..7e6c59d 100644 --- a/pkg/tools/get_data_access_data_source/tool.go +++ b/pkg/tools/get_data_access_data_source/tool.go @@ -26,10 +26,11 @@ type Output struct { func NewTool(collibraClient *http.Client) *chip.Tool[Input, Output] { return &chip.Tool[Input, Output]{ Name: "get_data_access_data_source", + Title: "Get Data Access Data Source", Description: "Fetches a Collibra Data Access data source by its ID, returning its name, type, description, parent, and timestamps. Use this to resolve a data source ID (the dataSourceId carried by data objects from search_data_access_objects and check_user_data_object_access) to a human-readable data source name and type. If the ID does not correspond to an existing data source, a message asking the user to correct it is returned.", Handler: handle(collibraClient), Permissions: []string{}, - Annotations: &mcp.ToolAnnotations{ReadOnlyHint: true, DestructiveHint: new(false)}, + Annotations: &mcp.ToolAnnotations{ReadOnlyHint: true, DestructiveHint: new(false), IdempotentHint: true, OpenWorldHint: new(false)}, } } diff --git a/pkg/tools/search_data_access_identities/tool.go b/pkg/tools/search_data_access_identities/tool.go index e1cd8d4..9051a11 100644 --- a/pkg/tools/search_data_access_identities/tool.go +++ b/pkg/tools/search_data_access_identities/tool.go @@ -24,10 +24,11 @@ type SearchDataAccessIdentitiesOutput struct { func NewTool(collibraClient *http.Client) *chip.Tool[SearchDataAccessIdentitiesInput, SearchDataAccessIdentitiesOutput] { return &chip.Tool[SearchDataAccessIdentitiesInput, SearchDataAccessIdentitiesOutput]{ Name: "search_data_access_identities", + Title: "Search Data Access Identities", Description: "Search for Data Access users (identities) by name and/or email. Providing email performs an exact lookup; providing name performs a case-insensitive contains search via ListUsers. Both can be combined: email resolves the user, name filters the result client-side.", Handler: handleSearchDataAccessIdentities(collibraClient), Permissions: []string{}, - Annotations: &mcp.ToolAnnotations{ReadOnlyHint: true, DestructiveHint: new(false)}, + Annotations: &mcp.ToolAnnotations{ReadOnlyHint: true, DestructiveHint: new(false), IdempotentHint: true, OpenWorldHint: new(false)}, } } diff --git a/pkg/tools/search_data_access_objects/tool.go b/pkg/tools/search_data_access_objects/tool.go index cf79e1a..ed781fd 100644 --- a/pkg/tools/search_data_access_objects/tool.go +++ b/pkg/tools/search_data_access_objects/tool.go @@ -28,10 +28,11 @@ type SearchDataAccessObjectsOutput struct { func NewTool(collibraClient *http.Client) *chip.Tool[SearchDataAccessObjectsInput, SearchDataAccessObjectsOutput] { return &chip.Tool[SearchDataAccessObjectsInput, SearchDataAccessObjectsOutput]{ Name: "search_data_access_objects", + Title: "Search Data Access Objects", Description: "Search for data objects in Collibra Data Access. Data objects represent tables, columns, schemas, views, and other entities tracked in registered data sources. Filters can be combined: name (case-insensitive contains), dataSources (data source IDs), types (e.g. table, column), parents/ancestors (other data object IDs), and includeDeleted. Returns up to pageSize matches (default 25, max 25). Each result also includes its applicablePermissions — the source-system permissions (with name and description) that can be requested on the object.", Handler: handleSearchDataAccessObjects(collibraClient), Permissions: []string{}, - Annotations: &mcp.ToolAnnotations{ReadOnlyHint: true, DestructiveHint: new(false)}, + Annotations: &mcp.ToolAnnotations{ReadOnlyHint: true, DestructiveHint: new(false), IdempotentHint: true, OpenWorldHint: new(false)}, } } From a1648653b1598be7d62ba4d688743ba7323025cc Mon Sep 17 00:00:00 2001 From: Wouter Cordewiner <73988312+wouterc-collibra@users.noreply.github.com> Date: Wed, 5 Aug 2026 23:12:43 +0200 Subject: [PATCH 13/13] ci: bump golangci-lint to v2.12.2, fix redundant embedded selectors golangci-lint v2.4.0 was built with go1.25 and refuses to lint a module targeting go1.26.5. Bumping to v2.12.2 (built with a newer toolchain) unblocks the Lint step. Also fixes the staticcheck QF1008 findings it then surfaced: several genqlient-generated SDK types embed a field with the same name as the struct (e.g. AccessControl.AccessControl.Id), so the outer field name can be dropped from the selector. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/build.yaml | 2 +- pkg/clients/data_access_client.go | 36 +++++++++++++++---------------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 6ca271b..df5b6a6 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -40,7 +40,7 @@ jobs: - name: Lint uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8 with: - version: v2.4.0 + version: v2.12.2 # Every green build of main is tagged with the next patch version. # The release workflow then publishes a chosen tag on demand. diff --git a/pkg/clients/data_access_client.go b/pkg/clients/data_access_client.go index e362a1c..887c819 100644 --- a/pkg/clients/data_access_client.go +++ b/pkg/clients/data_access_client.go @@ -123,10 +123,10 @@ func mapToDataAccessWhatItem(w *types.AccessWhatAccessControlItem) DataAccessWha ExpiresAt: w.ExpiresAt, } if w.AccessControl != nil { - item.ID = w.AccessControl.AccessControl.Id - item.Name = w.AccessControl.AccessControl.Name - item.State = string(w.AccessControl.AccessControl.State) - item.Action = string(w.AccessControl.AccessControl.Action) + item.ID = w.AccessControl.Id + item.Name = w.AccessControl.Name + item.State = string(w.AccessControl.State) + item.Action = string(w.AccessControl.Action) } return item } @@ -141,10 +141,10 @@ func mapToDataAccessWhoItem(w *types.AccessWhoItem) DataAccessWhoItem { switch v := w.Item.(type) { case *types.AccessWhoItemItemUser: item.ItemType = "User" - item.ItemID = v.User.Id - item.ItemName = v.User.Name - item.Email = v.User.Email - item.UserType = string(v.User.Type) + item.ItemID = v.Id + item.ItemName = v.Name + item.Email = v.Email + item.UserType = string(v.Type) case *types.AccessWhoItemItemAccessControl: item.ItemType = "AccessControl" item.ItemID = v.Id @@ -508,11 +508,11 @@ func mapToUserDataObjectAccess(item *types.GroupedDataAccessReturnItem) *UserDat } if ac.Category != nil { role.Category = &DataAccessGrantCategory{ - ID: ac.Category.GrantCategory.Id, - Name: ac.Category.GrantCategory.Name, - NamePlural: ac.Category.GrantCategory.NamePlural, - IsSystem: ac.Category.GrantCategory.IsSystem, - IsDefault: ac.Category.GrantCategory.IsDefault, + ID: ac.Category.Id, + Name: ac.Category.Name, + NamePlural: ac.Category.NamePlural, + IsSystem: ac.Category.IsSystem, + IsDefault: ac.Category.IsDefault, } } access.Roles = append(access.Roles, role) @@ -678,11 +678,11 @@ func mapToDataAccessControlDetails(ctx context.Context, ac *types.AccessControl) if ac.Category != nil { details.Category = &DataAccessGrantCategory{ - ID: ac.Category.GrantCategory.Id, - Name: ac.Category.GrantCategory.Name, - NamePlural: ac.Category.GrantCategory.NamePlural, - IsSystem: ac.Category.GrantCategory.IsSystem, - IsDefault: ac.Category.GrantCategory.IsDefault, + ID: ac.Category.Id, + Name: ac.Category.Name, + NamePlural: ac.Category.NamePlural, + IsSystem: ac.Category.IsSystem, + IsDefault: ac.Category.IsDefault, } }