Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Bug report: Cannot add an owner to a site if I don't have access to the site already #4600

Open
martinlingstuyl opened this issue Mar 1, 2023 · 27 comments · May be fixed by #4861
Open

Bug report: Cannot add an owner to a site if I don't have access to the site already #4600

martinlingstuyl opened this issue Mar 1, 2023 · 27 comments · May be fixed by #4861

Comments

@martinlingstuyl
Copy link
Contributor

martinlingstuyl commented Mar 1, 2023

Description

Some time ago we refactored the spo site classic set into the spo site set command and combined group site and regular site setting operations.

This has resulted in an issue. We can no longer set owners for a site if we are currently not an owner or site member of that site.

In other words, if I am a SharePoint administrator, I sometimes want to give myself access to site collections using scripts. But now I cannot.

This command is affected:
https://pnp.github.io/cli-microsoft365/cmd/spo/site/site-set/

The reason for this situation is that the CLI first tries to retrieve if the site is a Group or not, right here

It does that using the site specific api, which it cannot access because the logged in user has no access yet.

I'm not sure how we should fix this yet. We had some thoughts on specific CRUD actions for Sitecollection admins, but these are currently nowhere near completion: #4173

What we could do is start using the tenant api to get the site details, (just like spo site list)

Steps to reproduce

Try to add your own account as an owner to a site you have no access to:

m365 spo site set --url "https://contoso.sharepoint.com/sites/some-site" --owners [email protected]

Implementation

We've decided to switch from the /sites/somesite/_api/site to use an admin API. We'll want to use the following API call to fetch the right information:

POST https://tenant-admin.sharepoint.com/_api/SPO.Tenant/RenderAdminListData

data:

{
    "parameters": {
        "ViewXml": "<View><Query><Where><Contains><FieldRef Name='SiteUrl'/><Value Type='Text'>https://tenant.sharepoint.com/sites/yoursite</Value></Contains></Where></Query><ViewFields><FieldRef Name=\"GroupId\"/><FieldRef Name=\"SiteId\"/><FieldRef Name=\"SiteUrl\"/></ViewFields></View>"
    }
}

We'll want to place this functionality in a util function, so we can reuse it in other places. I'd suggest we add the following function to the spo.ts util function:

/**
 * Retrieves a Custom Actions from a SharePoint site by Id.
 * @param adminUrl URL of the SharePoint admin site
 * @param camlQuery An optional viewQuery to add to the CAML query between the <Query> tags.
 * @param viewFields An optional array of internal names of fields to include in the response.
 */
getTenantSites(adminUrl: string, camlQuery?: string, viewFields?: string[])
@martinlingstuyl martinlingstuyl changed the title Bug report: Cannot set owner of a site if I am not a site member Bug report: Cannot add an owner to a site if I don't have access to the site already Mar 1, 2023
@milanholemans
Copy link
Contributor

I think it should be possible for both owners of the site and admin users. Perhaps we have to add an --asAdmin flag like we do for pp commands.

@martinlingstuyl
Copy link
Contributor Author

We would still need to check if the site is a group or not though...

By the way I think the command seems to almost always need admin privileges, so an extra flag should not be necessary.

@nicodecleyre
Copy link
Contributor

Can it be an option to use the graph batch api to add an owner to a site where you don't have access to?

POST https://graph.microsoft.com/v1.0/$batch

{
    "requests": [
        {
            "id": "AddMember_0",
            "method": "POST",
            "url": "/groups/[GROUP_ID]/owners/$ref",
            "body": {
                "@odata.id": "https://graph.microsoft.com/v1.0/users/[USER_ID]"
            },
            "headers": {
                "content-type": "application/json"
            }
        },
        {
            "id": "AddMember_1",
            "method": "POST",
            "url": "/groups/[GROUP_ID]/owners/$ref",
            "body": {
                "@odata.id": "https://graph.microsoft.com/v1.0/users/[USER_ID]"
            },
            "headers": {
                "content-type": "application/json"
            }
        }
    ]
}

@martinlingstuyl
Copy link
Contributor Author

The point is that we dont have the groupId yet. As soon as we would have it, the current implementation works fine, I believe.

@nicodecleyre
Copy link
Contributor

nicodecleyre commented Mar 1, 2023

The point is that we dont have the groupId yet. As soon as we would have it, the current implementation works fine, I believe.

Got it, then we can use this api:

POST https://tenant-admin.sharepoint.com/_api/SPO.Tenant/RenderAdminListData

data:

{
    "parameters": {
        "ViewXml": "<View><Query><Where><Contains><FieldRef Name='SiteUrl'/><Value Type='Text'>https://tenant.sharepoint.com/sites/yoursite</Value></Contains></Where></Query><ViewFields><FieldRef Name=\"GroupId\"/><FieldRef Name=\"SiteId\"/><FieldRef Name=\"SiteUrl\"/></ViewFields></View>"
    }
}

But it would need an accesstoken for the admin site resource though

image

@martinlingstuyl
Copy link
Contributor Author

We could also reuse ${spoAdminUrl}/_vti_bin/client.svc/ProcessQuery as we do in spo site list, the access token is no problem I think..

@martinlingstuyl
Copy link
Contributor Author

Well, you need to add the xml envelope specific to this endpoint.... (check out site-list.ts)

@nicodecleyre
Copy link
Contributor

Well, you need to add the xml envelope specific to this endpoint.... (check out site-list.ts)

I tried to use the xml envelope in combination with the ${spoAdminUrl}/_vti_bin/client.svc/ProcessQuery. But i'm getting an 'invalid request' response. Am I doing something wrong here?

the data:

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
    <soapenv:Body>
        <GetListItems xmlns="http://schemas.microsoft.com/sharepoint/soap/">
            <listName>DO_NOT_DELETE_SPLIST_TENANTADMIN_AGGREGATED_SITECOLLECTIONS</listName>
            <ViewFields>
                <FieldRef Name="GroupId"/>
                <FieldRef Name="SiteId"/>
                <FieldRef Name="SiteUrl"/>
            </ViewFields>
            <View>
                <Query>
                    <Where>
                        <Contains>
                            <FieldRef Name="SiteUrl"/>
                            <Value Type="Text">https://tenant.sharepoint.com/sites/yoursite</Value>
                        </Contains>
                    </Where>
                </Query>
            </View>
        </GetListItems>
    </soapenv:Body>
</soapenv:Envelope>

@nicodecleyre
Copy link
Contributor

nicodecleyre commented Mar 1, 2023

Also noticed we can do a get request to get the group id:

GET https://tenant-admin.sharepoint.com/_api/web/lists/getbytitle('DO_NOT_DELETE_SPLIST_TENANTADMIN_AGGREGATED_SITECOLLECTIONS')/items?$select=Title,SiteId,SiteUrl,GroupId&$filter=SiteUrl eq 'https://tenant.sharepoint.com/sites/yoursite'

@nicodecleyre
Copy link
Contributor

Well, you need to add the xml envelope specific to this endpoint.... (check out site-list.ts)

I tried to use the xml envelope in combination with the ${spoAdminUrl}/_vti_bin/client.svc/ProcessQuery. But i'm getting an 'invalid request' response. Am I doing something wrong here?

the data:

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
    <soapenv:Body>
        <GetListItems xmlns="http://schemas.microsoft.com/sharepoint/soap/">
            <listName>DO_NOT_DELETE_SPLIST_TENANTADMIN_AGGREGATED_SITECOLLECTIONS</listName>
            <ViewFields>
                <FieldRef Name="GroupId"/>
                <FieldRef Name="SiteId"/>
                <FieldRef Name="SiteUrl"/>
            </ViewFields>
            <View>
                <Query>
                    <Where>
                        <Contains>
                            <FieldRef Name="SiteUrl"/>
                            <Value Type="Text">https://tenant.sharepoint.com/sites/yoursite</Value>
                        </Contains>
                    </Where>
                </Query>
            </View>
        </GetListItems>
    </soapenv:Body>
</soapenv:Envelope>

This seems to work regarding the ${spoAdminUrl}/_vti_bin/client.svc/ProcessQuery api.

<Request AddExpandoFieldTypeSuffix="true" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName="CLI for Microsoft 365 v6.4.0" xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009">
    <Actions>
        <ObjectPath Id="2" ObjectPathId="1"/>
        <ObjectPath Id="4" ObjectPathId="3"/>
        <Query Id="5" ObjectPathId="3">
            <Query>
                <Properties/>
            </Query>
            <ChildItemQuery>
                <Properties>
                    <Property Name="GroupId"/>
                </Properties>
            </ChildItemQuery>
        </Query>
    </Actions>
    <ObjectPaths>
        <Constructor Id="1" TypeId="{268004ae-ef6b-4e9b-8425-127220d84719}"/>
        <Method Id="3" ParentId="1" Name="GetSitePropertiesFromSharePointByFilters">
            <Parameters>
                <Parameter TypeId="{b92aeee2-c92c-4b67-abcc-024e471bc140}">
                    <Property Name="Filter" Type="String">Url -eq 'https://tenant.sharepoint.com/sites/yoursite'</Property>
                    <Property Name="IncludeDetail" Type="Boolean">false</Property>
                    <Property Name="IncludePersonalSite" Type="Enum">0</Property>
                    <Property Name="StartIndex" Type="String">0</Property>
                    <Property Name="Template" Type="String"></Property>
                </Parameter>
            </Parameters>
        </Method>
    </ObjectPaths>
</Request>

image

But it doesn't seem to recognize the SiteId property (tried to show all the properties but it doesn't show a site id either.. 😞)

image

@martinlingstuyl
Copy link
Contributor Author

Maybe just an id? It should have one, although it might be named different..

@nicodecleyre
Copy link
Contributor

Maybe just an id? It should have one, although it might be named different..

doesn't seem so.. The same goes for spo site list, it doesn't display an id

@nicodecleyre
Copy link
Contributor

So tested this out. We can fix this by using the get api https://tenant-admin.sharepoint.com/_api/web/lists/getbytitle('DO_NOT_DELETE_SPLIST_TENANTADMIN_AGGREGATED_SITECOLLECTIONS')/items?$select=Title,SiteId,SiteUrl,GroupId&$filter=SiteUrl eq 'https://tenant.sharepoint.com/sites/yoursite' to retrieve the groupId and siteId for sites where we don't have access to. So the loadSiteIds function would look like this:
image

The situation as is regarding a site where you don't have access to:
image

The situation with the get api regarding a site where you don't have access to (siteId & groupId temporary logged to show that they get retrieved):
image

@martinlingstuyl
Copy link
Contributor Author

Hi @nicodecleyre, the only thing that I'm slightly concerned about with this way of doing things is the 5000-item 'limit' on SharePoint.

With the tenant endpoint, SP will take care of loading and filtering. If we do it ourselves using this list, we might run into these annoying errors with large tenants.

@nicodecleyre
Copy link
Contributor

Let's stick then with the first option and use RenderAdminListData, it's the official api that the ui uses in the new SharePoint admin portal and it's a wrapper over the RenderListDataAsStream, which can handle the 5000 threshold limit..

@martinlingstuyl
Copy link
Contributor Author

Does it contain a SiteId?

@nicodecleyre
Copy link
Contributor

Does it contain a SiteId?

Yes

@martinlingstuyl
Copy link
Contributor Author

Ok, interesting, does it allow to $select properties to optimize the request? If so, we could use it.

Maybe we could check how the output relates to the current spo site list output. We might move this into a util function and use it in both places.

For example: getTenantSites(adminUrl:string, filter:string, select:string)

@nicodecleyre
Copy link
Contributor

Ok, interesting, does it allow to $select properties to optimize the request? If so, we could use it.

No, it ignores the $select query parameter. The selection of the properties happen in the ViewFields element like in the example above

Maybe we could check how the output relates to the current spo site list output. We might move this into a util function and use it in both places.

For example: getTenantSites(adminUrl:string, filter:string, select:string)

Also no, the properties the 2 api's return don't match 100%. The /_api/site (which is currently used by the spo site list command) returns properties that the RenderAdminListData doesn't return and visa versa. However, we could use the RenderAdminListData api to refactor the spo hubsite list where currently the /_api/web/lists/GetByTitle('DO_NOT_DELETE_SPLIST_TENANTADMIN_AGGREGATED_SITECOLLECTIONS')/RenderListDataAsStream api is used. But it won't change a thing regarding getting or not getting the site when you don't have access to it, it will just make the code look nicer

@martinlingstuyl
Copy link
Contributor Author

martinlingstuyl commented Mar 6, 2023

the RenderListDataAsStream, which can handle the 5000 threshold limit..

By the way, out of curiosity, are you sure RenderListDataAsStream can handle the threshold limit? It's basically an endpoint to send CAML queries to. CAML queries can also run into threshold limit errors. Do you have documentation to support this?

Aside from that, I agree with you that we should use the wrapper endpoint, as it's used by the SP user interface as well and as it contains the correct properties.

Also no, the properties the 2 api's return don't match 100%. The /_api/site (which is currently used by the spo site list command) returns properties that the RenderAdminListData doesn't return and visa versa.

In that case: let's leave the spo site list command, as it's working right now and there's no need to refactor it, certainly not when this would incur a breaking change.

However, we could use the RenderAdminListData api to refactor the spo hubsite list where currently the /_api/web/lists/GetByTitle('DO_NOT_DELETE_SPLIST_TENANTADMIN_AGGREGATED_SITECOLLECTIONS')/RenderListDataAsStream api is used.

I like this idea. Also, because that will remove the need to use executeCommandWithOutput in that command. Let's create a util function while we're fixing this bug. I'll rework the specs.

@martinlingstuyl martinlingstuyl added needs peer review Needs second pair of eyes to review the spec or PR and removed needs research labels Mar 6, 2023
@martinlingstuyl
Copy link
Contributor Author

@pnp/cli-for-microsoft-365-maintainers any comments on the renewed specs, based on the discussion?

@martinlingstuyl
Copy link
Contributor Author

Thanks for the research @nicodecleyre!

@nicodecleyre
Copy link
Contributor

By the way, out of curiosity, are you sure RenderListDataAsStream can handle the threshold limit? It's basically an endpoint to send CAML queries to. CAML queries can also run into threshold limit errors. Do you have documentation to support this?

No official documentation found regarding that. Only that it's the recommended approach when retrieving info from large lists and some blog posts that say it can handle the threshold limit

@Adam-it
Copy link
Member

Adam-it commented Mar 16, 2023

@pnp/cli-for-microsoft-365-maintainers any comments on the renewed specs, based on the discussion?

first of all sorry for the late response 🙏
I checked the discussion. Seems like all what I could think of was already checked 😅.
seems like a solid plan 🤔. +1

@martinlingstuyl martinlingstuyl added help wanted and removed needs peer review Needs second pair of eyes to review the spec or PR labels May 10, 2023
@martinlingstuyl
Copy link
Contributor Author

let's get this shipped!

@nicodecleyre
Copy link
Contributor

Something I can work on?

@martinlingstuyl
Copy link
Contributor Author

Sure @nicodecleyre!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
4 participants