Line data Source code
1 : // Copyright (C) 2013 The Android Open Source Project 2 : // 3 : // Licensed under the Apache License, Version 2.0 (the "License"); 4 : // you may not use this file except in compliance with the License. 5 : // You may obtain a copy of the License at 6 : // 7 : // http://www.apache.org/licenses/LICENSE-2.0 8 : // 9 : // Unless required by applicable law or agreed to in writing, software 10 : // distributed under the License is distributed on an "AS IS" BASIS, 11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 : // See the License for the specific language governing permissions and 13 : // limitations under the License. 14 : 15 : package com.google.gerrit.server.restapi.group; 16 : 17 : import com.google.common.base.Strings; 18 : import com.google.common.collect.Lists; 19 : import com.google.common.collect.Sets; 20 : import com.google.gerrit.entities.Account; 21 : import com.google.gerrit.entities.AccountGroup; 22 : import com.google.gerrit.entities.GroupDescription; 23 : import com.google.gerrit.exceptions.NoSuchGroupException; 24 : import com.google.gerrit.extensions.client.AuthType; 25 : import com.google.gerrit.extensions.common.AccountInfo; 26 : import com.google.gerrit.extensions.restapi.AuthException; 27 : import com.google.gerrit.extensions.restapi.DefaultInput; 28 : import com.google.gerrit.extensions.restapi.IdString; 29 : import com.google.gerrit.extensions.restapi.ResourceNotFoundException; 30 : import com.google.gerrit.extensions.restapi.Response; 31 : import com.google.gerrit.extensions.restapi.RestApiException; 32 : import com.google.gerrit.extensions.restapi.RestCollectionCreateView; 33 : import com.google.gerrit.extensions.restapi.RestModifyView; 34 : import com.google.gerrit.extensions.restapi.UnprocessableEntityException; 35 : import com.google.gerrit.server.UserInitiated; 36 : import com.google.gerrit.server.account.AccountCache; 37 : import com.google.gerrit.server.account.AccountException; 38 : import com.google.gerrit.server.account.AccountLoader; 39 : import com.google.gerrit.server.account.AccountManager; 40 : import com.google.gerrit.server.account.AccountResolver; 41 : import com.google.gerrit.server.account.AccountResolver.UnresolvableAccountException; 42 : import com.google.gerrit.server.account.AccountState; 43 : import com.google.gerrit.server.account.AuthRequest; 44 : import com.google.gerrit.server.account.GroupControl; 45 : import com.google.gerrit.server.account.externalids.ExternalId; 46 : import com.google.gerrit.server.config.AuthConfig; 47 : import com.google.gerrit.server.group.GroupResource; 48 : import com.google.gerrit.server.group.MemberResource; 49 : import com.google.gerrit.server.group.db.GroupDelta; 50 : import com.google.gerrit.server.group.db.GroupsUpdate; 51 : import com.google.gerrit.server.permissions.PermissionBackendException; 52 : import com.google.gerrit.server.restapi.group.AddMembers.Input; 53 : import com.google.inject.Inject; 54 : import com.google.inject.Provider; 55 : import com.google.inject.Singleton; 56 : import java.io.IOException; 57 : import java.util.ArrayList; 58 : import java.util.LinkedHashSet; 59 : import java.util.List; 60 : import java.util.Optional; 61 : import java.util.Set; 62 : import org.eclipse.jgit.errors.ConfigInvalidException; 63 : 64 : @Singleton 65 : public class AddMembers implements RestModifyView<GroupResource, Input> { 66 13 : public static class Input { 67 : @DefaultInput String _oneMember; 68 : 69 : List<String> members; 70 : 71 : public static Input fromMembers(List<String> members) { 72 12 : Input in = new Input(); 73 12 : in.members = members; 74 12 : return in; 75 : } 76 : 77 : static Input init(Input in) { 78 13 : if (in == null) { 79 0 : in = new Input(); 80 : } 81 13 : if (in.members == null) { 82 2 : in.members = Lists.newArrayListWithCapacity(1); 83 : } 84 13 : if (!Strings.isNullOrEmpty(in._oneMember)) { 85 2 : in.members.add(in._oneMember); 86 : } 87 13 : return in; 88 : } 89 : } 90 : 91 : private final AccountManager accountManager; 92 : private final AuthType authType; 93 : private final AccountResolver accountResolver; 94 : private final AccountCache accountCache; 95 : private final AccountLoader.Factory infoFactory; 96 : private final Provider<GroupsUpdate> groupsUpdateProvider; 97 : private final AuthRequest.Factory authRequestFactory; 98 : 99 : @Inject 100 : AddMembers( 101 : AccountManager accountManager, 102 : AuthConfig authConfig, 103 : AccountResolver accountResolver, 104 : AccountCache accountCache, 105 : AccountLoader.Factory infoFactory, 106 : @UserInitiated Provider<GroupsUpdate> groupsUpdateProvider, 107 149 : AuthRequest.Factory authRequestFactory) { 108 149 : this.accountManager = accountManager; 109 149 : this.authType = authConfig.getAuthType(); 110 149 : this.accountResolver = accountResolver; 111 149 : this.accountCache = accountCache; 112 149 : this.infoFactory = infoFactory; 113 149 : this.groupsUpdateProvider = groupsUpdateProvider; 114 149 : this.authRequestFactory = authRequestFactory; 115 149 : } 116 : 117 : @Override 118 : public Response<List<AccountInfo>> apply(GroupResource resource, Input input) 119 : throws AuthException, NotInternalGroupException, UnprocessableEntityException, IOException, 120 : ConfigInvalidException, ResourceNotFoundException, PermissionBackendException { 121 13 : GroupDescription.Internal internalGroup = 122 13 : resource.asInternalGroup().orElseThrow(NotInternalGroupException::new); 123 13 : input = Input.init(input); 124 : 125 13 : GroupControl control = resource.getControl(); 126 13 : if (!control.canAddMember()) { 127 0 : throw new AuthException("Cannot add members to group " + internalGroup.getName()); 128 : } 129 : 130 13 : Set<Account.Id> newMemberIds = new LinkedHashSet<>(); 131 13 : for (String nameOrEmailOrId : input.members) { 132 12 : Account a = findAccount(nameOrEmailOrId); 133 12 : if (!a.isActive()) { 134 0 : throw new UnprocessableEntityException( 135 0 : String.format("Account Inactive: %s", nameOrEmailOrId)); 136 : } 137 12 : newMemberIds.add(a.id()); 138 12 : } 139 : 140 12 : AccountGroup.UUID groupUuid = internalGroup.getGroupUUID(); 141 : try { 142 12 : addMembers(groupUuid, newMemberIds); 143 0 : } catch (NoSuchGroupException e) { 144 0 : throw new ResourceNotFoundException(String.format("Group %s not found", groupUuid), e); 145 12 : } 146 12 : return Response.ok(toAccountInfoList(newMemberIds)); 147 : } 148 : 149 : Account findAccount(String nameOrEmailOrId) 150 : throws UnprocessableEntityException, IOException, ConfigInvalidException { 151 27 : AccountResolver.Result result = accountResolver.resolve(nameOrEmailOrId); 152 : try { 153 26 : return result.asUnique().account(); 154 2 : } catch (UnresolvableAccountException e) { 155 2 : switch (authType) { 156 : case HTTP_LDAP: 157 : case CLIENT_SSL_CERT_LDAP: 158 : case LDAP: 159 0 : if (!e.isSelf() && result.asList().isEmpty()) { 160 : // Account does not exist, try to create it. This may leak account existence, since we 161 : // can't distinguish between a nonexistent account and one that the caller can't see. 162 0 : Optional<Account> a = createAccountByLdap(nameOrEmailOrId); 163 0 : if (a.isPresent()) { 164 0 : return a.get(); 165 : } 166 0 : } 167 : break; 168 : case CUSTOM_EXTENSION: 169 : case DEVELOPMENT_BECOME_ANY_ACCOUNT: 170 : case HTTP: 171 : case LDAP_BIND: 172 : case OAUTH: 173 : case OPENID: 174 : case OPENID_SSO: 175 : default: 176 : } 177 2 : throw e; 178 : } 179 : } 180 : 181 : public void addMembers(AccountGroup.UUID groupUuid, Set<Account.Id> newMemberIds) 182 : throws IOException, NoSuchGroupException, ConfigInvalidException { 183 : GroupDelta groupDelta = 184 12 : GroupDelta.builder() 185 12 : .setMemberModification(memberIds -> Sets.union(memberIds, newMemberIds)) 186 12 : .build(); 187 12 : groupsUpdateProvider.get().updateGroup(groupUuid, groupDelta); 188 12 : } 189 : 190 : private Optional<Account> createAccountByLdap(String user) throws IOException { 191 0 : if (!ExternalId.isValidUsername(user)) { 192 0 : return Optional.empty(); 193 : } 194 : 195 : try { 196 0 : AuthRequest req = authRequestFactory.createForUser(user); 197 0 : req.setSkipAuthentication(true); 198 0 : return accountCache 199 0 : .get(accountManager.authenticate(req).getAccountId()) 200 0 : .map(AccountState::account); 201 0 : } catch (AccountException e) { 202 0 : return Optional.empty(); 203 : } 204 : } 205 : 206 : private List<AccountInfo> toAccountInfoList(Set<Account.Id> accountIds) 207 : throws PermissionBackendException { 208 12 : List<AccountInfo> result = new ArrayList<>(); 209 12 : AccountLoader loader = infoFactory.create(true); 210 12 : for (Account.Id accId : accountIds) { 211 12 : result.add(loader.get(accId)); 212 12 : } 213 12 : loader.fill(); 214 12 : return result; 215 : } 216 : 217 : @Singleton 218 : public static class CreateMember 219 : implements RestCollectionCreateView<GroupResource, MemberResource, Input> { 220 : private final AddMembers put; 221 : 222 : @Inject 223 138 : public CreateMember(AddMembers put) { 224 138 : this.put = put; 225 138 : } 226 : 227 : @Override 228 : public Response<AccountInfo> apply(GroupResource resource, IdString id, Input input) 229 : throws RestApiException, NotInternalGroupException, IOException, ConfigInvalidException, 230 : PermissionBackendException { 231 1 : AddMembers.Input in = new AddMembers.Input(); 232 1 : in._oneMember = id.get(); 233 : try { 234 0 : List<AccountInfo> list = put.apply(resource, in).value(); 235 0 : if (list.size() == 1) { 236 0 : return Response.created(list.get(0)); 237 : } 238 0 : throw new IllegalStateException(); 239 1 : } catch (UnprocessableEntityException e) { 240 1 : throw new ResourceNotFoundException(id, e); 241 : } 242 : } 243 : } 244 : 245 : @Singleton 246 : public static class UpdateMember implements RestModifyView<MemberResource, Input> { 247 : private final GetMember get; 248 : 249 : @Inject 250 138 : public UpdateMember(GetMember get) { 251 138 : this.get = get; 252 138 : } 253 : 254 : @Override 255 : public Response<AccountInfo> apply(MemberResource resource, Input input) 256 : throws PermissionBackendException { 257 : // Do nothing, the user is already a member. 258 1 : return get.apply(resource); 259 : } 260 : } 261 : }